@reconcrap/boss-recommend-mcp 1.3.32 → 1.3.34

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.
@@ -1,475 +1,674 @@
1
- import assert from "node:assert/strict";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdir } from "node:fs/promises";
6
-
7
- import {
8
- cancelBossChatRun,
9
- getBossChatHealthCheck,
10
- getBossChatRun,
11
- pauseBossChatRun,
12
- prepareBossChatRun,
13
- resumeBossChatRun,
14
- startBossChatRun
15
- } from "./boss-chat.js";
16
- import { __testables as cliTestables } from "./cli.js";
17
- import { __testables as indexTestables } from "./index.js";
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdir } from "node:fs/promises";
6
+
7
+ import {
8
+ cancelBossChatRun,
9
+ getBossChatHealthCheck,
10
+ getBossChatRun,
11
+ pauseBossChatRun,
12
+ prepareBossChatRun,
13
+ resumeBossChatRun,
14
+ startBossChatRun
15
+ } from "./boss-chat.js";
16
+ import { __testables as cliTestables } from "./cli.js";
17
+ import { __testables as indexTestables } from "./index.js";
18
18
  import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
19
19
  import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
20
20
  import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
21
+ import { ChromeClient } from "../vendor/boss-chat-cli/src/services/chrome-client.js";
21
22
  import { LlmClient, parseLlmJson, __testables as llmTestables } from "../vendor/boss-chat-cli/src/services/llm.js";
22
23
  import { ReportStore } from "../vendor/boss-chat-cli/src/services/report-store.js";
23
- import {
24
- NETWORK_RESUME_IMAGE_MODE_GRACE_MS,
25
- NETWORK_RESUME_RETRY_WAIT_MS,
26
- NETWORK_RESUME_WAIT_MS,
27
- ResumeNetworkTracker,
28
- } from "../vendor/boss-chat-cli/src/services/resume-network.js";
29
-
30
- const { handleRequest } = indexTestables;
31
-
32
- const TOOL_BOSS_CHAT_HEALTH_CHECK = "boss_chat_health_check";
33
- const TOOL_BOSS_CHAT_PREPARE_RUN = "prepare_boss_chat_run";
34
- const TOOL_BOSS_CHAT_START_RUN = "start_boss_chat_run";
35
- const TOOL_BOSS_CHAT_GET_RUN = "get_boss_chat_run";
36
- const TOOL_BOSS_CHAT_PAUSE_RUN = "pause_boss_chat_run";
37
- const TOOL_BOSS_CHAT_RESUME_RUN = "resume_boss_chat_run";
38
- const TOOL_BOSS_CHAT_CANCEL_RUN = "cancel_boss_chat_run";
39
- const { extractCompletionReasoningText, extractResponsesReasoningText } = llmTestables;
40
-
41
- function makeToolCall(id, name, args = {}) {
42
- return {
43
- jsonrpc: "2.0",
44
- id,
45
- method: "tools/call",
46
- params: {
47
- name,
48
- arguments: args
24
+ import {
25
+ NETWORK_RESUME_IMAGE_MODE_GRACE_MS,
26
+ NETWORK_RESUME_RETRY_WAIT_MS,
27
+ NETWORK_RESUME_WAIT_MS,
28
+ ResumeNetworkTracker,
29
+ } from "../vendor/boss-chat-cli/src/services/resume-network.js";
30
+
31
+ const { handleRequest } = indexTestables;
32
+
33
+ const TOOL_BOSS_CHAT_HEALTH_CHECK = "boss_chat_health_check";
34
+ const TOOL_BOSS_CHAT_PREPARE_RUN = "prepare_boss_chat_run";
35
+ const TOOL_BOSS_CHAT_START_RUN = "start_boss_chat_run";
36
+ const TOOL_BOSS_CHAT_GET_RUN = "get_boss_chat_run";
37
+ const TOOL_BOSS_CHAT_PAUSE_RUN = "pause_boss_chat_run";
38
+ const TOOL_BOSS_CHAT_RESUME_RUN = "resume_boss_chat_run";
39
+ const TOOL_BOSS_CHAT_CANCEL_RUN = "cancel_boss_chat_run";
40
+ const { extractCompletionReasoningText, extractResponsesReasoningText } = llmTestables;
41
+
42
+ function makeToolCall(id, name, args = {}) {
43
+ return {
44
+ jsonrpc: "2.0",
45
+ id,
46
+ method: "tools/call",
47
+ params: {
48
+ name,
49
+ arguments: args
50
+ }
51
+ };
52
+ }
53
+
54
+ async function callTool(workspaceRoot, name, args = {}, id = 1) {
55
+ const response = await handleRequest(makeToolCall(id, name, args), workspaceRoot);
56
+ return response?.result?.structuredContent;
57
+ }
58
+
59
+ function createBossChatTestWorkspace() {
60
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
61
+ const configDir = path.join(workspaceRoot, "config");
62
+ const cliDir = path.join(workspaceRoot, "boss-chat-cli", "src");
63
+ fs.mkdirSync(configDir, { recursive: true });
64
+ fs.mkdirSync(cliDir, { recursive: true });
65
+
66
+ fs.writeFileSync(path.join(configDir, "screening-config.json"), JSON.stringify({
67
+ baseUrl: "https://api.example.com/v1",
68
+ apiKey: "sk-test-key",
69
+ model: "gpt-4.1-mini",
70
+ llmTimeoutMs: 65000,
71
+ llmMaxRetries: 4,
72
+ debugPort: 9666
73
+ }, null, 2));
74
+
75
+ fs.writeFileSync(path.join(cliDir, "cli.js"), [
76
+ "#!/usr/bin/env node",
77
+ "const fs = require('node:fs');",
78
+ "const path = require('node:path');",
79
+ "const cwd = process.cwd();",
80
+ "const statePath = path.join(cwd, '.boss-chat', 'stub-state.json');",
81
+ "fs.mkdirSync(path.dirname(statePath), { recursive: true });",
82
+ "const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
83
+ "const state = JSON.parse(raw || '{}');",
84
+ "state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
85
+ "state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
86
+ "if (!Number.isInteger(state.prepare_fail_budget)) {",
87
+ " const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
88
+ " state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
89
+ "}",
90
+ "state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
91
+ "state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
92
+ "const argv = process.argv.slice(2);",
93
+ "const command = String(argv[0] || '').trim();",
94
+ "const options = {};",
95
+ "for (let index = 1; index < argv.length; index += 1) {",
96
+ " const token = String(argv[index] || '');",
97
+ " if (!token.startsWith('--')) continue;",
98
+ " const key = token.slice(2);",
99
+ " const next = argv[index + 1];",
100
+ " if (next && !String(next).startsWith('--')) {",
101
+ " options[key] = String(next);",
102
+ " index += 1;",
103
+ " } else {",
104
+ " options[key] = true;",
105
+ " }",
106
+ "}",
107
+ "function saveAndPrint(payload) {",
108
+ " fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
109
+ " process.stdout.write(`${JSON.stringify(payload)}\\n`);",
110
+ "}",
111
+ "if (command === 'prepare-run') {",
112
+ " state.prepare_calls += 1;",
113
+ " if (state.prepare_fail_budget > 0) {",
114
+ " state.prepare_fail_budget -= 1;",
115
+ " saveAndPrint({ status: 'FAILED', error: { code: 'CHAT_PAGE_NOT_READY', message: 'chat page is still loading' } });",
116
+ " process.exit(1);",
117
+ " }",
118
+ " state.last_prepare_args = options;",
119
+ " saveAndPrint({",
120
+ " status: 'NEED_INPUT',",
121
+ " stage: 'chat_run_setup',",
122
+ " page_url: 'https://www.zhipin.com/web/chat/index',",
123
+ " required_fields: ['job', 'start_from', 'target_count', 'criteria'],",
124
+ " job_options: [",
125
+ " { index: 1, label: '算法工程师', value: '算法工程师', active: true },",
126
+ " { index: 2, label: '大模型算法', value: '大模型算法', active: false }",
127
+ " ],",
128
+ " pending_questions: [",
129
+ " { field: 'job', question: '请选择岗位(必须从岗位列表中选择)', required: true },",
130
+ " { field: 'start_from', question: '请选择起始范围', required: true },",
131
+ " { field: 'target_count', question: '请输入目标数量(正整数)', required: true },",
132
+ " { field: 'criteria', question: '请输入筛选标准(自然语言)', required: true }",
133
+ " ],",
134
+ " message: 'prepared'",
135
+ " });",
136
+ " process.exit(0);",
137
+ "}",
138
+ "if (command === 'start-run') {",
139
+ " state.counter += 1;",
140
+ " const runId = `chat-${state.counter}`;",
141
+ " state.last_start_args = options;",
142
+ " state.runs[runId] = { state: 'queued' };",
143
+ " state.get_calls[runId] = 0;",
144
+ " saveAndPrint({ status: 'ACCEPTED', run_id: runId, message: 'chat started' });",
145
+ " process.exit(0);",
146
+ "}",
147
+ "const runId = String(options['run-id'] || '');",
148
+ "const current = state.runs[runId] || { state: 'queued' };",
149
+ "if (command === 'get-run') {",
150
+ " state.get_calls[runId] = (state.get_calls[runId] || 0) + 1;",
151
+ " if (!['paused', 'canceled'].includes(current.state)) {",
152
+ " current.state = state.get_calls[runId] >= 2 ? 'completed' : 'running';",
153
+ " }",
154
+ " state.runs[runId] = current;",
155
+ " saveAndPrint({",
156
+ " status: 'RUN_STATUS',",
157
+ " run: {",
158
+ " runId,",
159
+ " state: current.state,",
160
+ " lastMessage: `state=${current.state}`,",
161
+ " progress: { inspected: state.get_calls[runId], passed: current.state === 'completed' ? 1 : 0, requested: current.state === 'completed' ? 1 : 0, skipped: 0, errors: 0 },",
162
+ " result: current.state === 'completed' ? { requested_count: 1 } : null",
163
+ " }",
164
+ " });",
165
+ " process.exit(0);",
166
+ "}",
167
+ "if (command === 'pause-run') {",
168
+ " current.state = 'paused';",
169
+ " state.runs[runId] = current;",
170
+ " saveAndPrint({ status: 'PAUSE_REQUESTED', run: { runId, state: 'paused' } });",
171
+ " process.exit(0);",
172
+ "}",
173
+ "if (command === 'resume-run') {",
174
+ " current.state = 'running';",
175
+ " state.runs[runId] = current;",
176
+ " saveAndPrint({ status: 'RESUME_REQUESTED', run: { runId, state: 'running' } });",
177
+ " process.exit(0);",
178
+ "}",
179
+ "if (command === 'cancel-run') {",
180
+ " current.state = 'canceled';",
181
+ " state.runs[runId] = current;",
182
+ " saveAndPrint({ status: 'CANCEL_REQUESTED', run: { runId, state: 'canceled' } });",
183
+ " process.exit(0);",
184
+ "}",
185
+ "saveAndPrint({ status: 'FAILED', error: { code: 'UNKNOWN_COMMAND', message: command || 'missing command' } });",
186
+ "process.exit(1);"
187
+ ].join("\n"), "utf8");
188
+
189
+ return workspaceRoot;
190
+ }
191
+
192
+ function readStubState(workspaceRoot) {
193
+ const statePath = path.join(workspaceRoot, ".boss-chat", "stub-state.json");
194
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
195
+ }
196
+
197
+ async function withBossChatWorkspace(testFn) {
198
+ const workspaceRoot = createBossChatTestWorkspace();
199
+ const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
200
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
201
+ try {
202
+ await testFn(workspaceRoot);
203
+ } finally {
204
+ if (previousScreenConfig === undefined) {
205
+ delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
206
+ } else {
207
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
208
+ }
209
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
210
+ }
211
+ }
212
+
213
+ async function captureConsoleLogs(fn) {
214
+ const messages = [];
215
+ const originalLog = console.log;
216
+ console.log = (...args) => {
217
+ messages.push(args.join(" "));
218
+ };
219
+ try {
220
+ await fn();
221
+ } finally {
222
+ console.log = originalLog;
223
+ }
224
+ return messages;
225
+ }
226
+
227
+ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
228
+ await withBossChatWorkspace(async (workspaceRoot) => {
229
+ const health = getBossChatHealthCheck(workspaceRoot);
230
+ assert.equal(health.status, "OK");
231
+ assert.equal(health.shared_llm_config, true);
232
+ assert.equal(health.debug_port, 9666);
233
+
234
+ const prepared = await prepareBossChatRun({
235
+ workspaceRoot,
236
+ input: {}
237
+ });
238
+ assert.equal(prepared.status, "NEED_INPUT");
239
+ assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
240
+ const preparedTargetQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
241
+ assert.equal(preparedTargetQuestion.argument_name, "target_count");
242
+ assert.equal(preparedTargetQuestion.recommended_value, "all");
243
+ assert.equal(preparedTargetQuestion.recommended_argument_patch.target_count, "all");
244
+ assert.equal(preparedTargetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
245
+ assert.equal(prepared.next_call_example.target_count, "all");
246
+
247
+ const preflight = await startBossChatRun({
248
+ workspaceRoot,
249
+ input: {}
250
+ });
251
+ assert.equal(preflight.status, "NEED_INPUT");
252
+ assert.deepEqual(preflight.required_fields, ["job", "start_from", "target_count", "criteria"]);
253
+ assert.equal(Array.isArray(preflight.job_options), true);
254
+ assert.equal(preflight.job_options.length, 2);
255
+ assert.equal(Array.isArray(preflight.pending_questions), true);
256
+ const preflightTargetQuestion = preflight.pending_questions.find((item) => item.field === "target_count");
257
+ assert.equal(Boolean(preflightTargetQuestion), true);
258
+ assert.equal(preflightTargetQuestion.argument_name, "target_count");
259
+ assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
260
+ assert.equal(Array.isArray(preflightTargetQuestion.options), true);
261
+
262
+ const stateAfterPrepare = readStubState(workspaceRoot);
263
+ assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
264
+ assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
265
+ assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
266
+ assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
267
+ assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
268
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-timeout-ms"], "65000");
269
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-max-retries"], "4");
270
+
271
+ const started = await startBossChatRun({
272
+ workspaceRoot,
273
+ input: {
274
+ profile: "default",
275
+ job: "算法工程师",
276
+ start_from: "unread",
277
+ criteria: "有 AI Agent 经验",
278
+ target_count: 2
279
+ }
280
+ });
281
+ assert.equal(started.status, "ACCEPTED");
282
+ assert.equal(Boolean(started.run_id), true);
283
+
284
+ const stateAfterStart = readStubState(workspaceRoot);
285
+ assert.equal(stateAfterStart.last_start_args.profile, "default");
286
+ assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
287
+ assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
288
+ assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
289
+ assert.equal(stateAfterStart.last_start_args.targetCount, "2");
290
+ assert.equal(stateAfterStart.last_start_args.baseurl, "https://api.example.com/v1");
291
+ assert.equal(stateAfterStart.last_start_args.apikey, "sk-test-key");
292
+ assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
293
+ assert.equal(stateAfterStart.last_start_args.port, "9666");
294
+ assert.equal(stateAfterStart.last_start_args["llm-timeout-ms"], "65000");
295
+ assert.equal(stateAfterStart.last_start_args["llm-max-retries"], "4");
296
+
297
+ const startedAll = await startBossChatRun({
298
+ workspaceRoot,
299
+ input: {
300
+ profile: "default",
301
+ job: "算法工程师",
302
+ start_from: "all",
303
+ criteria: "全部候选人都过一遍",
304
+ target_count: "全部候选人"
305
+ }
306
+ });
307
+ assert.equal(startedAll.status, "ACCEPTED");
308
+ const stateAfterStartAll = readStubState(workspaceRoot);
309
+ assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
310
+
311
+ for (const target_count of ["all", -1, "-1", { value: "all" }, "all(扫到底)"]) {
312
+ const startedVariant = await startBossChatRun({
313
+ workspaceRoot,
314
+ input: {
315
+ profile: "default",
316
+ job: "算法工程师",
317
+ start_from: "all",
318
+ criteria: "全部候选人都过一遍",
319
+ target_count
320
+ }
321
+ });
322
+ assert.equal(startedVariant.status, "ACCEPTED");
323
+ assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
324
+ }
325
+
326
+ const startedCamelCase = await startBossChatRun({
327
+ workspaceRoot,
328
+ input: {
329
+ profile: "default",
330
+ job: "算法工程师",
331
+ start_from: "all",
332
+ criteria: "全部候选人都过一遍",
333
+ targetCount: { targetCount: "all" }
334
+ }
335
+ });
336
+ assert.equal(startedCamelCase.status, "ACCEPTED");
337
+ assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
338
+
339
+ const invalidTarget = await startBossChatRun({
340
+ workspaceRoot,
341
+ input: {
342
+ profile: "default",
343
+ job: "算法工程师",
344
+ start_from: "all",
345
+ criteria: "全部候选人都过一遍",
346
+ target_count: "not a target"
347
+ }
348
+ });
349
+ assert.equal(invalidTarget.status, "NEED_INPUT");
350
+ assert.deepEqual(invalidTarget.missing_fields, ["target_count"]);
351
+ assert.equal(invalidTarget.received_target_count, "not a target");
352
+ assert.equal(Boolean(invalidTarget.target_count_parse_error), true);
353
+ assert.equal(invalidTarget.next_call_example.target_count, "all");
354
+ assert.equal(invalidTarget.accepted_examples.includes("all"), true);
355
+ assert.equal(invalidTarget.recommended_argument_patch.target_count, "all");
356
+
357
+ const running = await getBossChatRun({
358
+ workspaceRoot,
359
+ input: {
360
+ profile: "default",
361
+ run_id: started.run_id
362
+ }
363
+ });
364
+ assert.equal(running.run.state, "running");
365
+
366
+ const paused = await pauseBossChatRun({
367
+ workspaceRoot,
368
+ input: {
369
+ profile: "default",
370
+ run_id: started.run_id
371
+ }
372
+ });
373
+ assert.equal(paused.run.state, "paused");
374
+
375
+ const resumed = await resumeBossChatRun({
376
+ workspaceRoot,
377
+ input: {
378
+ profile: "default",
379
+ run_id: started.run_id
380
+ }
381
+ });
382
+ assert.equal(resumed.run.state, "running");
383
+
384
+ const canceled = await cancelBossChatRun({
385
+ workspaceRoot,
386
+ input: {
387
+ profile: "default",
388
+ run_id: started.run_id
389
+ }
390
+ });
391
+ assert.equal(canceled.run.state, "canceled");
392
+ });
393
+ }
394
+
395
+ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
396
+ await withBossChatWorkspace(async (workspaceRoot) => {
397
+ const previousPrepareFails = process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
398
+ process.env.BOSS_CHAT_STUB_PREPARE_FAILS = "2";
399
+ try {
400
+ const prepared = await prepareBossChatRun({
401
+ workspaceRoot,
402
+ input: {}
403
+ });
404
+ assert.equal(prepared.status, "NEED_INPUT");
405
+ const state = readStubState(workspaceRoot);
406
+ assert.equal(state.prepare_calls, 3);
407
+ assert.equal(state.prepare_fail_budget, 0);
408
+ } finally {
409
+ if (previousPrepareFails === undefined) {
410
+ delete process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
411
+ } else {
412
+ process.env.BOSS_CHAT_STUB_PREPARE_FAILS = previousPrepareFails;
413
+ }
414
+ }
415
+ });
416
+ }
417
+
418
+ async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
419
+ const fakeChromeClient = {
420
+ async callFunction() {
421
+ return {
422
+ href: "https://www.zhipin.com/web/chat/index",
423
+ readyState: "complete",
424
+ hasListContainer: false,
425
+ listItemCount: 0
426
+ };
427
+ }
428
+ };
429
+
430
+ const page = new BossChatPage(fakeChromeClient);
431
+ const pageState = await page.ensureOnChatPage();
432
+ assert.equal(pageState.href, "https://www.zhipin.com/web/chat/index");
433
+
434
+ await assert.rejects(
435
+ () => page.ensureReady(),
436
+ /CHAT_LIST_CONTAINER_NOT_FOUND/
437
+ );
438
+ }
439
+
440
+ async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad() {
441
+ const calls = [];
442
+ let stateIndex = 0;
443
+ const states = [
444
+ {
445
+ href: "https://www.zhipin.com/web/chat/index",
446
+ readyState: "loading",
447
+ hasListContainer: false,
448
+ listItemCount: 0
449
+ },
450
+ {
451
+ href: "https://www.zhipin.com/web/chat/index",
452
+ readyState: "interactive",
453
+ hasListContainer: false,
454
+ listItemCount: 0
455
+ },
456
+ {
457
+ href: "https://www.zhipin.com/web/chat/index",
458
+ readyState: "complete",
459
+ hasListContainer: false,
460
+ listItemCount: 0
461
+ }
462
+ ];
463
+
464
+ const fakeChromeClient = {
465
+ async callFunction(fn, arg) {
466
+ calls.push({ name: fn.name, arg });
467
+ if (fn.name === "browserGetCurrentHref") {
468
+ return { href: "https://www.zhipin.com/web/chat/index" };
469
+ }
470
+ if (fn.name === "browserNavigateToChatIndex") {
471
+ return { ok: true, changed: true, href: "https://www.zhipin.com/web/chat/index" };
472
+ }
473
+ if (fn.name === "browserGetPageState") {
474
+ const value = states[Math.min(stateIndex, states.length - 1)];
475
+ stateIndex += 1;
476
+ return value;
477
+ }
478
+ throw new Error(`unexpected function: ${fn.name}`);
479
+ }
480
+ };
481
+
482
+ const page = new BossChatPage(fakeChromeClient);
483
+ const result = await page.recoverToChatIndex({
484
+ forceNavigate: true,
485
+ waitForReadyState: "complete",
486
+ maxAttempts: 5,
487
+ delayMs: 0
488
+ });
489
+
490
+ assert.equal(result.changed, true);
491
+ assert.equal(result.href, "https://www.zhipin.com/web/chat/index");
492
+ assert.equal(
493
+ calls.some((entry) => entry.name === "browserNavigateToChatIndex" && entry.arg?.force === true),
494
+ true
495
+ );
496
+ assert.equal(
497
+ calls.filter((entry) => entry.name === "browserGetPageState").length >= 3,
498
+ true
499
+ );
500
+ }
501
+
502
+ async function testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail() {
503
+ const calls = [];
504
+ const mouseEvents = [];
505
+ let stateIndex = 0;
506
+ const openState = {
507
+ open: true,
508
+ panelCount: 1,
509
+ closeCount: 1,
510
+ topPanelClass: "new-resume-online-main-ui",
511
+ topPanelScore: 520,
512
+ panelRect: {
513
+ left: 980,
514
+ top: 0,
515
+ width: 330,
516
+ height: 760,
517
+ right: 1310,
518
+ bottom: 760
519
+ },
520
+ closeRect: {
521
+ left: 1274,
522
+ top: 12,
523
+ width: 30,
524
+ height: 30,
525
+ right: 1304,
526
+ bottom: 42
527
+ },
528
+ overlayClass: "dialog-wrap active",
529
+ overlayRect: {
530
+ left: 0,
531
+ top: 0,
532
+ width: 1440,
533
+ height: 773,
534
+ right: 1440,
535
+ bottom: 773
536
+ },
537
+ contentClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
538
+ contentRect: {
539
+ left: 980,
540
+ top: 0,
541
+ width: 330,
542
+ height: 760,
543
+ right: 1310,
544
+ bottom: 760
49
545
  }
50
546
  };
51
- }
52
-
53
- async function callTool(workspaceRoot, name, args = {}, id = 1) {
54
- const response = await handleRequest(makeToolCall(id, name, args), workspaceRoot);
55
- return response?.result?.structuredContent;
56
- }
57
-
58
- function createBossChatTestWorkspace() {
59
- const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
60
- const configDir = path.join(workspaceRoot, "config");
61
- const cliDir = path.join(workspaceRoot, "boss-chat-cli", "src");
62
- fs.mkdirSync(configDir, { recursive: true });
63
- fs.mkdirSync(cliDir, { recursive: true });
64
-
65
- fs.writeFileSync(path.join(configDir, "screening-config.json"), JSON.stringify({
66
- baseUrl: "https://api.example.com/v1",
67
- apiKey: "sk-test-key",
68
- model: "gpt-4.1-mini",
69
- llmTimeoutMs: 65000,
70
- llmMaxRetries: 4,
71
- debugPort: 9666
72
- }, null, 2));
73
-
74
- fs.writeFileSync(path.join(cliDir, "cli.js"), [
75
- "#!/usr/bin/env node",
76
- "const fs = require('node:fs');",
77
- "const path = require('node:path');",
78
- "const cwd = process.cwd();",
79
- "const statePath = path.join(cwd, '.boss-chat', 'stub-state.json');",
80
- "fs.mkdirSync(path.dirname(statePath), { recursive: true });",
81
- "const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
82
- "const state = JSON.parse(raw || '{}');",
83
- "state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
84
- "state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
85
- "if (!Number.isInteger(state.prepare_fail_budget)) {",
86
- " const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
87
- " state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
88
- "}",
89
- "state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
90
- "state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
91
- "const argv = process.argv.slice(2);",
92
- "const command = String(argv[0] || '').trim();",
93
- "const options = {};",
94
- "for (let index = 1; index < argv.length; index += 1) {",
95
- " const token = String(argv[index] || '');",
96
- " if (!token.startsWith('--')) continue;",
97
- " const key = token.slice(2);",
98
- " const next = argv[index + 1];",
99
- " if (next && !String(next).startsWith('--')) {",
100
- " options[key] = String(next);",
101
- " index += 1;",
102
- " } else {",
103
- " options[key] = true;",
104
- " }",
105
- "}",
106
- "function saveAndPrint(payload) {",
107
- " fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
108
- " process.stdout.write(`${JSON.stringify(payload)}\\n`);",
109
- "}",
110
- "if (command === 'prepare-run') {",
111
- " state.prepare_calls += 1;",
112
- " if (state.prepare_fail_budget > 0) {",
113
- " state.prepare_fail_budget -= 1;",
114
- " saveAndPrint({ status: 'FAILED', error: { code: 'CHAT_PAGE_NOT_READY', message: 'chat page is still loading' } });",
115
- " process.exit(1);",
116
- " }",
117
- " state.last_prepare_args = options;",
118
- " saveAndPrint({",
119
- " status: 'NEED_INPUT',",
120
- " stage: 'chat_run_setup',",
121
- " page_url: 'https://www.zhipin.com/web/chat/index',",
122
- " required_fields: ['job', 'start_from', 'target_count', 'criteria'],",
123
- " job_options: [",
124
- " { index: 1, label: '算法工程师', value: '算法工程师', active: true },",
125
- " { index: 2, label: '大模型算法', value: '大模型算法', active: false }",
126
- " ],",
127
- " pending_questions: [",
128
- " { field: 'job', question: '请选择岗位(必须从岗位列表中选择)', required: true },",
129
- " { field: 'start_from', question: '请选择起始范围', required: true },",
130
- " { field: 'target_count', question: '请输入目标数量(正整数)', required: true },",
131
- " { field: 'criteria', question: '请输入筛选标准(自然语言)', required: true }",
132
- " ],",
133
- " message: 'prepared'",
134
- " });",
135
- " process.exit(0);",
136
- "}",
137
- "if (command === 'start-run') {",
138
- " state.counter += 1;",
139
- " const runId = `chat-${state.counter}`;",
140
- " state.last_start_args = options;",
141
- " state.runs[runId] = { state: 'queued' };",
142
- " state.get_calls[runId] = 0;",
143
- " saveAndPrint({ status: 'ACCEPTED', run_id: runId, message: 'chat started' });",
144
- " process.exit(0);",
145
- "}",
146
- "const runId = String(options['run-id'] || '');",
147
- "const current = state.runs[runId] || { state: 'queued' };",
148
- "if (command === 'get-run') {",
149
- " state.get_calls[runId] = (state.get_calls[runId] || 0) + 1;",
150
- " if (!['paused', 'canceled'].includes(current.state)) {",
151
- " current.state = state.get_calls[runId] >= 2 ? 'completed' : 'running';",
152
- " }",
153
- " state.runs[runId] = current;",
154
- " saveAndPrint({",
155
- " status: 'RUN_STATUS',",
156
- " run: {",
157
- " runId,",
158
- " state: current.state,",
159
- " lastMessage: `state=${current.state}`,",
160
- " progress: { inspected: state.get_calls[runId], passed: current.state === 'completed' ? 1 : 0, requested: current.state === 'completed' ? 1 : 0, skipped: 0, errors: 0 },",
161
- " result: current.state === 'completed' ? { requested_count: 1 } : null",
162
- " }",
163
- " });",
164
- " process.exit(0);",
165
- "}",
166
- "if (command === 'pause-run') {",
167
- " current.state = 'paused';",
168
- " state.runs[runId] = current;",
169
- " saveAndPrint({ status: 'PAUSE_REQUESTED', run: { runId, state: 'paused' } });",
170
- " process.exit(0);",
171
- "}",
172
- "if (command === 'resume-run') {",
173
- " current.state = 'running';",
174
- " state.runs[runId] = current;",
175
- " saveAndPrint({ status: 'RESUME_REQUESTED', run: { runId, state: 'running' } });",
176
- " process.exit(0);",
177
- "}",
178
- "if (command === 'cancel-run') {",
179
- " current.state = 'canceled';",
180
- " state.runs[runId] = current;",
181
- " saveAndPrint({ status: 'CANCEL_REQUESTED', run: { runId, state: 'canceled' } });",
182
- " process.exit(0);",
183
- "}",
184
- "saveAndPrint({ status: 'FAILED', error: { code: 'UNKNOWN_COMMAND', message: command || 'missing command' } });",
185
- "process.exit(1);"
186
- ].join("\n"), "utf8");
187
-
188
- return workspaceRoot;
189
- }
190
-
191
- function readStubState(workspaceRoot) {
192
- const statePath = path.join(workspaceRoot, ".boss-chat", "stub-state.json");
193
- return JSON.parse(fs.readFileSync(statePath, "utf8"));
194
- }
195
-
196
- async function withBossChatWorkspace(testFn) {
197
- const workspaceRoot = createBossChatTestWorkspace();
198
- const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
199
- process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
200
- try {
201
- await testFn(workspaceRoot);
202
- } finally {
203
- if (previousScreenConfig === undefined) {
204
- delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
205
- } else {
206
- process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
547
+ const states = [
548
+ openState,
549
+ openState,
550
+ openState,
551
+ {
552
+ open: false,
553
+ panelCount: 0,
554
+ closeCount: 0,
555
+ topPanelClass: "",
556
+ topPanelScore: 0,
557
+ panelRect: null,
558
+ closeRect: null,
559
+ overlayClass: "",
560
+ overlayRect: null,
561
+ contentClass: "",
562
+ contentRect: null
207
563
  }
208
- fs.rmSync(workspaceRoot, { recursive: true, force: true });
209
- }
210
- }
211
-
212
- async function captureConsoleLogs(fn) {
213
- const messages = [];
214
- const originalLog = console.log;
215
- console.log = (...args) => {
216
- messages.push(args.join(" "));
217
- };
218
- try {
219
- await fn();
220
- } finally {
221
- console.log = originalLog;
222
- }
223
- return messages;
224
- }
225
-
226
- async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
227
- await withBossChatWorkspace(async (workspaceRoot) => {
228
- const health = getBossChatHealthCheck(workspaceRoot);
229
- assert.equal(health.status, "OK");
230
- assert.equal(health.shared_llm_config, true);
231
- assert.equal(health.debug_port, 9666);
232
-
233
- const prepared = await prepareBossChatRun({
234
- workspaceRoot,
235
- input: {}
236
- });
237
- assert.equal(prepared.status, "NEED_INPUT");
238
- assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
239
- const preparedTargetQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
240
- assert.equal(preparedTargetQuestion.argument_name, "target_count");
241
- assert.equal(preparedTargetQuestion.recommended_value, "all");
242
- assert.equal(preparedTargetQuestion.recommended_argument_patch.target_count, "all");
243
- assert.equal(preparedTargetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
244
- assert.equal(prepared.next_call_example.target_count, "all");
245
-
246
- const preflight = await startBossChatRun({
247
- workspaceRoot,
248
- input: {}
249
- });
250
- assert.equal(preflight.status, "NEED_INPUT");
251
- assert.deepEqual(preflight.required_fields, ["job", "start_from", "target_count", "criteria"]);
252
- assert.equal(Array.isArray(preflight.job_options), true);
253
- assert.equal(preflight.job_options.length, 2);
254
- assert.equal(Array.isArray(preflight.pending_questions), true);
255
- const preflightTargetQuestion = preflight.pending_questions.find((item) => item.field === "target_count");
256
- assert.equal(Boolean(preflightTargetQuestion), true);
257
- assert.equal(preflightTargetQuestion.argument_name, "target_count");
258
- assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
259
- assert.equal(Array.isArray(preflightTargetQuestion.options), true);
260
-
261
- const stateAfterPrepare = readStubState(workspaceRoot);
262
- assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
263
- assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
264
- assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
265
- assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
266
- assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
267
- assert.equal(stateAfterPrepare.last_prepare_args["llm-timeout-ms"], "65000");
268
- assert.equal(stateAfterPrepare.last_prepare_args["llm-max-retries"], "4");
269
-
270
- const started = await startBossChatRun({
271
- workspaceRoot,
272
- input: {
273
- profile: "default",
274
- job: "算法工程师",
275
- start_from: "unread",
276
- criteria: "有 AI Agent 经验",
277
- target_count: 2
564
+ ];
565
+
566
+ const fakeChromeClient = {
567
+ Input: {
568
+ async dispatchMouseEvent(payload) {
569
+ mouseEvents.push(payload);
570
+ }
571
+ },
572
+ async pressEscape() {
573
+ calls.push("pressEscape");
574
+ },
575
+ async callFunction(fn) {
576
+ calls.push(fn.name);
577
+ if (fn.name === "browserIsCandidateDetailOpen") {
578
+ const value = states[Math.min(stateIndex, states.length - 1)];
579
+ stateIndex += 1;
580
+ return value;
581
+ }
582
+ if (fn.name === "browserCloseCandidateDetailDomOnce") {
583
+ return {
584
+ ok: true,
585
+ selector: ".close-btn",
586
+ method: "dom-click-once"
587
+ };
278
588
  }
279
- });
280
- assert.equal(started.status, "ACCEPTED");
281
- assert.equal(Boolean(started.run_id), true);
282
-
283
- const stateAfterStart = readStubState(workspaceRoot);
284
- assert.equal(stateAfterStart.last_start_args.profile, "default");
285
- assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
286
- assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
287
- assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
288
- assert.equal(stateAfterStart.last_start_args.targetCount, "2");
289
- assert.equal(stateAfterStart.last_start_args.baseurl, "https://api.example.com/v1");
290
- assert.equal(stateAfterStart.last_start_args.apikey, "sk-test-key");
291
- assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
292
- assert.equal(stateAfterStart.last_start_args.port, "9666");
293
- assert.equal(stateAfterStart.last_start_args["llm-timeout-ms"], "65000");
294
- assert.equal(stateAfterStart.last_start_args["llm-max-retries"], "4");
295
-
296
- const startedAll = await startBossChatRun({
297
- workspaceRoot,
298
- input: {
299
- profile: "default",
300
- job: "算法工程师",
301
- start_from: "all",
302
- criteria: "全部候选人都过一遍",
303
- target_count: "全部候选人"
589
+ if (fn.name === "browserFindCandidateDetailOutsideClickPoint") {
590
+ return {
591
+ ok: true,
592
+ strategy: "left-gap",
593
+ point: { x: 72, y: 260 },
594
+ state: openState
595
+ };
304
596
  }
305
- });
306
- assert.equal(startedAll.status, "ACCEPTED");
307
- const stateAfterStartAll = readStubState(workspaceRoot);
308
- assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
309
-
310
- for (const target_count of ["all", -1, "-1", { value: "all" }, "all(扫到底)"]) {
311
- const startedVariant = await startBossChatRun({
312
- workspaceRoot,
313
- input: {
314
- profile: "default",
315
- job: "算法工程师",
316
- start_from: "all",
317
- criteria: "全部候选人都过一遍",
318
- target_count
319
- }
320
- });
321
- assert.equal(startedVariant.status, "ACCEPTED");
322
- assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
597
+ throw new Error(`unexpected function: ${fn.name}`);
323
598
  }
599
+ };
600
+
601
+ const page = new BossChatPage(fakeChromeClient);
602
+ const result = await page.closeCandidateDetail({
603
+ maxAttempts: 1,
604
+ ensureDismiss: true
605
+ });
324
606
 
325
- const startedCamelCase = await startBossChatRun({
326
- workspaceRoot,
327
- input: {
328
- profile: "default",
329
- job: "算法工程师",
330
- start_from: "all",
331
- criteria: "全部候选人都过一遍",
332
- targetCount: { targetCount: "all" }
333
- }
334
- });
335
- assert.equal(startedCamelCase.status, "ACCEPTED");
336
- assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
337
-
338
- const invalidTarget = await startBossChatRun({
339
- workspaceRoot,
340
- input: {
341
- profile: "default",
342
- job: "算法工程师",
343
- start_from: "all",
344
- criteria: "全部候选人都过一遍",
345
- target_count: "not a target"
346
- }
347
- });
348
- assert.equal(invalidTarget.status, "NEED_INPUT");
349
- assert.deepEqual(invalidTarget.missing_fields, ["target_count"]);
350
- assert.equal(invalidTarget.received_target_count, "not a target");
351
- assert.equal(Boolean(invalidTarget.target_count_parse_error), true);
352
- assert.equal(invalidTarget.next_call_example.target_count, "all");
353
- assert.equal(invalidTarget.accepted_examples.includes("all"), true);
354
- assert.equal(invalidTarget.recommended_argument_patch.target_count, "all");
355
-
356
- const running = await getBossChatRun({
357
- workspaceRoot,
358
- input: {
359
- profile: "default",
360
- run_id: started.run_id
361
- }
362
- });
363
- assert.equal(running.run.state, "running");
364
-
365
- const paused = await pauseBossChatRun({
366
- workspaceRoot,
367
- input: {
368
- profile: "default",
369
- run_id: started.run_id
370
- }
371
- });
372
- assert.equal(paused.run.state, "paused");
373
-
374
- const resumed = await resumeBossChatRun({
375
- workspaceRoot,
376
- input: {
377
- profile: "default",
378
- run_id: started.run_id
379
- }
380
- });
381
- assert.equal(resumed.run.state, "running");
382
-
383
- const canceled = await cancelBossChatRun({
384
- workspaceRoot,
385
- input: {
386
- profile: "default",
387
- run_id: started.run_id
388
- }
389
- });
390
- assert.equal(canceled.run.state, "canceled");
391
- });
607
+ assert.equal(result.closed, true);
608
+ assert.equal(calls.includes("pressEscape"), true);
609
+ assert.equal(mouseEvents.length > 0, true);
610
+ assert.equal(result.method.includes("outside-click:left-gap"), true);
392
611
  }
393
612
 
394
- async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
395
- await withBossChatWorkspace(async (workspaceRoot) => {
396
- const previousPrepareFails = process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
397
- process.env.BOSS_CHAT_STUB_PREPARE_FAILS = "2";
398
- try {
399
- const prepared = await prepareBossChatRun({
400
- workspaceRoot,
401
- input: {}
402
- });
403
- assert.equal(prepared.status, "NEED_INPUT");
404
- const state = readStubState(workspaceRoot);
405
- assert.equal(state.prepare_calls, 3);
406
- assert.equal(state.prepare_fail_budget, 0);
407
- } finally {
408
- if (previousPrepareFails === undefined) {
409
- delete process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
410
- } else {
411
- process.env.BOSS_CHAT_STUB_PREPARE_FAILS = previousPrepareFails;
412
- }
413
- }
414
- });
415
- }
613
+ async function testChromeClientShouldInjectBrowserHelpersIntoCallFunction() {
614
+ function helper(value) {
615
+ return Number(value || 0) + 1;
616
+ }
617
+ function target(value) {
618
+ return helper(value) * 2;
619
+ }
620
+ target.helpers = [helper];
416
621
 
417
- async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
418
- const fakeChromeClient = {
419
- async callFunction() {
420
- return {
421
- href: "https://www.zhipin.com/web/chat/index",
422
- readyState: "complete",
423
- hasListContainer: false,
424
- listItemCount: 0
425
- };
426
- }
622
+ const client = new ChromeClient(9222);
623
+ let capturedExpression = "";
624
+ client.evaluate = async (expression) => {
625
+ capturedExpression = String(expression || "");
626
+ return "ok";
427
627
  };
428
628
 
429
- const page = new BossChatPage(fakeChromeClient);
430
- const pageState = await page.ensureOnChatPage();
431
- assert.equal(pageState.href, "https://www.zhipin.com/web/chat/index");
629
+ const result = await client.callFunction(target, 2);
432
630
 
433
- await assert.rejects(
434
- () => page.ensureReady(),
435
- /CHAT_LIST_CONTAINER_NOT_FOUND/
436
- );
631
+ assert.equal(result, "ok");
632
+ assert.equal(capturedExpression.includes("function helper"), true);
633
+ assert.equal(capturedExpression.includes("function target"), true);
634
+ assert.equal(capturedExpression.includes("return (helper(value) * 2)") || capturedExpression.includes("return helper(value) * 2"), true);
437
635
  }
438
636
 
439
- async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad() {
637
+ async function testBossChatPageShouldWaitForPanelsClosedInStrictConversationReady() {
440
638
  const calls = [];
441
639
  let stateIndex = 0;
442
640
  const states = [
443
641
  {
444
- href: "https://www.zhipin.com/web/chat/index",
445
- readyState: "loading",
446
- hasListContainer: false,
447
- listItemCount: 0
448
- },
449
- {
450
- href: "https://www.zhipin.com/web/chat/index",
451
- readyState: "interactive",
452
- hasListContainer: false,
453
- listItemCount: 0
642
+ hasOnlineResume: true,
643
+ hasAskResume: true,
644
+ hasAttachmentResume: false,
645
+ attachmentResumeEnabled: false,
646
+ resumeModalOpen: false,
647
+ candidateDetailOpen: true,
648
+ panelsClosed: false,
649
+ editorVisible: true,
650
+ activeSubmit: true,
651
+ hasAnySubmit: true,
652
+ messageInputReady: true,
454
653
  },
455
654
  {
456
- href: "https://www.zhipin.com/web/chat/index",
457
- readyState: "complete",
458
- hasListContainer: false,
459
- listItemCount: 0
655
+ hasOnlineResume: true,
656
+ hasAskResume: true,
657
+ hasAttachmentResume: false,
658
+ attachmentResumeEnabled: false,
659
+ resumeModalOpen: false,
660
+ candidateDetailOpen: false,
661
+ panelsClosed: true,
662
+ editorVisible: true,
663
+ activeSubmit: true,
664
+ hasAnySubmit: true,
665
+ messageInputReady: true,
460
666
  }
461
667
  ];
462
-
463
668
  const fakeChromeClient = {
464
- async callFunction(fn, arg) {
465
- calls.push({ name: fn.name, arg });
466
- if (fn.name === "browserGetCurrentHref") {
467
- return { href: "https://www.zhipin.com/web/chat/index" };
468
- }
469
- if (fn.name === "browserNavigateToChatIndex") {
470
- return { ok: true, changed: true, href: "https://www.zhipin.com/web/chat/index" };
471
- }
472
- if (fn.name === "browserGetPageState") {
669
+ async callFunction(fn) {
670
+ calls.push(fn.name);
671
+ if (fn.name === "browserConversationReadyState") {
473
672
  const value = states[Math.min(stateIndex, states.length - 1)];
474
673
  stateIndex += 1;
475
674
  return value;
@@ -479,108 +678,60 @@ async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForComple
479
678
  };
480
679
 
481
680
  const page = new BossChatPage(fakeChromeClient);
482
- const result = await page.recoverToChatIndex({
483
- forceNavigate: true,
484
- waitForReadyState: "complete",
485
- maxAttempts: 5,
681
+ const result = await page.waitForConversationReady({
682
+ requirePanelsClosed: true,
683
+ maxAttempts: 3,
486
684
  delayMs: 0
487
685
  });
488
686
 
489
- assert.equal(result.changed, true);
490
- assert.equal(result.href, "https://www.zhipin.com/web/chat/index");
491
- assert.equal(
492
- calls.some((entry) => entry.name === "browserNavigateToChatIndex" && entry.arg?.force === true),
493
- true
494
- );
495
- assert.equal(
496
- calls.filter((entry) => entry.name === "browserGetPageState").length >= 3,
497
- true
498
- );
687
+ assert.equal(result.panelsClosed, true);
688
+ assert.equal(calls.filter((name) => name === "browserConversationReadyState").length, 2);
499
689
  }
500
690
 
501
- async function testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail() {
502
- const calls = [];
503
- const mouseEvents = [];
504
- let stateIndex = 0;
505
- const states = [
506
- {
507
- open: true,
508
- panelCount: 1,
509
- closeCount: 1,
510
- topPanelClass: "base-info-single-top-detail",
511
- topPanelScore: 520,
512
- panelRect: {
513
- left: 940,
514
- top: 60,
515
- width: 360,
516
- height: 720,
517
- right: 1300,
518
- bottom: 780
519
- },
520
- closeRect: {
521
- left: 1274,
522
- top: 12,
523
- width: 30,
524
- height: 30,
525
- right: 1304,
526
- bottom: 42
527
- }
528
- },
529
- {
530
- open: true,
531
- panelCount: 1,
532
- closeCount: 1,
533
- topPanelClass: "base-info-single-top-detail",
534
- topPanelScore: 520,
535
- panelRect: {
536
- left: 940,
537
- top: 60,
538
- width: 360,
539
- height: 720,
540
- right: 1300,
541
- bottom: 780
542
- },
543
- closeRect: {
544
- left: 1274,
545
- top: 12,
546
- width: 30,
547
- height: 30,
548
- right: 1304,
549
- bottom: 42
550
- }
551
- },
552
- {
553
- open: false,
554
- panelCount: 0,
555
- closeCount: 0,
556
- topPanelClass: "",
557
- topPanelScore: 0,
558
- panelRect: null,
559
- closeRect: null
560
- }
561
- ];
562
-
691
+ async function testBossChatPageShouldSurfaceCandidateDetailOverlayAndContentState() {
563
692
  const fakeChromeClient = {
564
- Input: {
565
- async dispatchMouseEvent(payload) {
566
- mouseEvents.push(payload);
567
- }
568
- },
569
- async pressEscape() {
570
- calls.push("pressEscape");
571
- },
572
693
  async callFunction(fn) {
573
- calls.push(fn.name);
574
694
  if (fn.name === "browserIsCandidateDetailOpen") {
575
- const value = states[Math.min(stateIndex, states.length - 1)];
576
- stateIndex += 1;
577
- return value;
578
- }
579
- if (fn.name === "browserCloseCandidateDetailDomOnce") {
580
695
  return {
581
- ok: true,
582
- selector: ".close-btn",
583
- method: "dom-click-once"
696
+ open: true,
697
+ panelCount: 1,
698
+ closeCount: 1,
699
+ topPanelClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
700
+ topPanelScore: 620,
701
+ panelRect: {
702
+ left: 980,
703
+ top: 0,
704
+ width: 330,
705
+ height: 760,
706
+ right: 1310,
707
+ bottom: 760
708
+ },
709
+ closeRect: {
710
+ left: 1274,
711
+ top: 12,
712
+ width: 30,
713
+ height: 30,
714
+ right: 1304,
715
+ bottom: 42
716
+ },
717
+ overlayClass: "dialog-wrap active",
718
+ overlayRect: {
719
+ left: 0,
720
+ top: 0,
721
+ width: 1440,
722
+ height: 773,
723
+ right: 1440,
724
+ bottom: 773
725
+ },
726
+ contentClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
727
+ contentRect: {
728
+ left: 980,
729
+ top: 0,
730
+ width: 330,
731
+ height: 760,
732
+ right: 1310,
733
+ bottom: 760
734
+ }
584
735
  };
585
736
  }
586
737
  throw new Error(`unexpected function: ${fn.name}`);
@@ -588,1158 +739,1224 @@ async function testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail(
588
739
  };
589
740
 
590
741
  const page = new BossChatPage(fakeChromeClient);
591
- const result = await page.closeCandidateDetail({
592
- maxAttempts: 1,
593
- ensureDismiss: true
594
- });
742
+ const result = await page.getCandidateDetailState();
595
743
 
596
- assert.equal(result.closed, true);
597
- assert.equal(calls.includes("pressEscape"), true);
598
- assert.equal(mouseEvents.length > 0, true);
744
+ assert.equal(result.overlayClass, "dialog-wrap active");
745
+ assert.equal(result.contentClass.includes("new-resume-online-main-ui"), true);
746
+ assert.equal(result.panelRect.left, result.contentRect.left);
747
+ assert.equal(result.overlayRect.width > result.contentRect.width, true);
599
748
  }
600
749
 
601
750
  async function testBossChatMcpToolsShouldValidateAndRoute() {
602
- await withBossChatWorkspace(async (workspaceRoot) => {
603
- const toolsResponse = await handleRequest({
604
- jsonrpc: "2.0",
605
- id: 10,
606
- method: "tools/list",
607
- params: {}
608
- }, workspaceRoot);
609
- const tools = toolsResponse.result.tools;
610
- const prepareToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_PREPARE_RUN).inputSchema;
611
- const startToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_START_RUN).inputSchema;
612
- assert.equal(prepareToolSchema.required, undefined);
613
- assert.deepEqual(startToolSchema.required, ["job", "start_from", "criteria"]);
614
- assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("target_count")), true);
615
- assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("targetCount")), true);
616
- assert.equal(startToolSchema.properties.target_count.examples.includes("all"), true);
617
- assert.equal(startToolSchema.examples.some((item) => item.target_count === "all"), true);
618
-
619
- const prepared = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PREPARE_RUN, {}, 101);
620
- assert.equal(prepared.status, "NEED_INPUT");
621
- assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
622
- const preparedTargetCountQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
623
- assert.equal(preparedTargetCountQuestion.argument_name, "target_count");
624
- assert.equal(preparedTargetCountQuestion.recommended_argument_patch.target_count, "all");
625
-
626
- const needInput = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {}, 11);
627
- assert.equal(needInput.status, "NEED_INPUT");
628
- assert.deepEqual(needInput.required_fields, ["job", "start_from", "target_count", "criteria"]);
629
- assert.equal(Array.isArray(needInput.job_options), true);
630
- assert.equal(needInput.job_options.length, 2);
631
- const targetQuestion = needInput.pending_questions.find((item) => item.field === "target_count");
632
- assert.equal(Boolean(targetQuestion), true);
633
- assert.equal(targetQuestion.argument_name, "target_count");
634
- assert.equal(targetQuestion.recommended_argument_patch.target_count, "all");
635
- assert.equal(targetQuestion.options.some((item) => item.value === "all"), true);
636
- assert.equal(targetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
637
-
638
- const missingTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
639
- job: "算法工程师",
640
- start_from: "all",
641
- criteria: "全部候选人都过一遍"
642
- }, 111);
643
- assert.equal(missingTargetOnly.status, "NEED_INPUT");
644
- assert.deepEqual(missingTargetOnly.missing_fields, ["target_count"]);
645
- assert.equal(missingTargetOnly.next_call_example.target_count, "all");
646
- assert.equal(missingTargetOnly.accepted_examples.includes(-1), true);
647
-
648
- const invalidTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
649
- job: "算法工程师",
650
- start_from: "all",
651
- criteria: "全部候选人都过一遍",
652
- target_count: "not a target"
653
- }, 112);
654
- assert.equal(invalidTargetOnly.status, "NEED_INPUT");
655
- assert.deepEqual(invalidTargetOnly.missing_fields, ["target_count"]);
656
- assert.equal(invalidTargetOnly.received_target_count, "not a target");
657
- assert.equal(Boolean(invalidTargetOnly.target_count_parse_error), true);
658
- assert.equal(invalidTargetOnly.next_call_example.target_count, "all");
659
- assert.equal(invalidTargetOnly.recommended_argument_patch.target_count, "all");
660
-
661
- const invalidStartResponse = await handleRequest(
662
- makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
663
- start_from: "invalid-value"
664
- }),
665
- workspaceRoot
666
- );
667
- assert.equal(invalidStartResponse.error.code, -32602);
668
-
669
- const invalidGetResponse = await handleRequest(
670
- makeToolCall(12, TOOL_BOSS_CHAT_GET_RUN, {}),
671
- workspaceRoot
672
- );
673
- assert.equal(invalidGetResponse.error.code, -32602);
674
-
675
- const health = await callTool(workspaceRoot, TOOL_BOSS_CHAT_HEALTH_CHECK, {}, 13);
676
- assert.equal(health.status, "OK");
677
-
678
- const started = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
679
- job: "算法工程师",
680
- start_from: "unread",
681
- criteria: "有 AI Agent 经验",
682
- target_count: 2
683
- }, 14);
684
- assert.equal(started.status, "ACCEPTED");
685
-
686
- const startedAll = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
687
- job: "算法工程师",
688
- start_from: "all",
689
- criteria: "全部候选人都过一遍",
690
- target_count: "全部候选人"
691
- }, 140);
692
- assert.equal(startedAll.status, "ACCEPTED");
693
- const stateAfterStartAll = readStubState(workspaceRoot);
694
- assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
695
-
696
- const startedCamelCase = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
697
- job: "算法工程师",
698
- start_from: "all",
699
- criteria: "全部候选人都过一遍",
700
- targetCount: "all"
701
- }, 141);
702
- assert.equal(startedCamelCase.status, "ACCEPTED");
703
- assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
704
-
705
- const running = await callTool(workspaceRoot, TOOL_BOSS_CHAT_GET_RUN, {
706
- run_id: started.run_id,
707
- profile: "default"
708
- }, 15);
709
- assert.equal(running.run.state, "running");
710
-
711
- const paused = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PAUSE_RUN, {
712
- run_id: started.run_id,
713
- profile: "default"
714
- }, 16);
715
- assert.equal(paused.run.state, "paused");
716
-
717
- const resumed = await callTool(workspaceRoot, TOOL_BOSS_CHAT_RESUME_RUN, {
718
- run_id: started.run_id,
719
- profile: "default"
720
- }, 17);
721
- assert.equal(resumed.run.state, "running");
722
-
723
- const canceled = await callTool(workspaceRoot, TOOL_BOSS_CHAT_CANCEL_RUN, {
724
- run_id: started.run_id,
725
- profile: "default"
726
- }, 18);
727
- assert.equal(canceled.run.state, "canceled");
728
- });
729
- }
730
-
731
- async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
732
- const followUpJson = cliTestables.getRunFollowUp({
733
- "follow-up-json": JSON.stringify({
734
- chat: {
735
- criteria: "有 AI Agent 经验",
736
- start_from: "unread",
737
- target_count: 2
738
- }
739
- })
740
- });
741
- assert.equal(followUpJson.chat.criteria, "有 AI Agent 经验");
742
- assert.equal(followUpJson.chat.target_count, 2);
743
-
744
- const tempFile = path.join(os.tmpdir(), `boss-recommend-follow-up-${Date.now()}.json`);
745
- fs.writeFileSync(tempFile, JSON.stringify({
746
- chat: {
747
- criteria: "熟悉 MCP",
748
- start_from: "all",
749
- target_count: 3
750
- }
751
- }, null, 2));
752
- try {
753
- const followUpFile = cliTestables.getRunFollowUp({
754
- "follow-up-file": tempFile
755
- });
756
- assert.equal(followUpFile.chat.criteria, "熟悉 MCP");
757
- assert.equal(followUpFile.chat.start_from, "all");
758
- } finally {
759
- fs.rmSync(tempFile, { force: true });
760
- }
761
-
762
- await withBossChatWorkspace(async (workspaceRoot) => {
763
- const prepareLogs = await captureConsoleLogs(async () => {
764
- await cliTestables.runBossChatCliCommand("prepare-run", {
765
- "workspace-root": workspaceRoot
766
- });
767
- });
768
- const prepared = JSON.parse(prepareLogs[0]);
769
- assert.equal(prepared.status, "NEED_INPUT");
770
- assert.equal(prepared.pending_questions.find((item) => item.field === "target_count").argument_name, "target_count");
771
-
772
- const logs = await captureConsoleLogs(async () => {
773
- await cliTestables.runBossChatCliCommand("run", {
774
- "workspace-root": workspaceRoot,
775
- job: "算法工程师",
776
- "start-from": "unread",
777
- criteria: "有 AI Agent 经验",
778
- targetCount: "2"
779
- });
780
- });
781
- assert.equal(logs.length > 0, true);
782
- const payload = JSON.parse(logs[0]);
783
- assert.equal(payload.status, "ACCEPTED");
784
- assert.equal(typeof payload.run_id, "string");
785
- const state = readStubState(workspaceRoot);
786
- assert.equal(state.get_calls[payload.run_id] || 0, 0);
787
-
788
- await captureConsoleLogs(async () => {
789
- await cliTestables.runBossChatCliCommand("run", {
790
- "workspace-root": workspaceRoot,
791
- job: "算法工程师",
792
- "start-from": "all",
793
- criteria: "全部候选人都过一遍",
794
- targetCount: "全部候选人"
795
- });
796
- });
797
- const allState = readStubState(workspaceRoot);
798
- assert.equal(allState.last_start_args.targetCount, "-1");
799
- });
800
- }
801
-
802
- async function testVendorBossChatCliShouldWaitForHydratedChatShell() {
803
- const pageStates = [
804
- { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
805
- { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
806
- { href: "https://www.zhipin.com/web/chat/index", hasListContainer: true, listItemCount: 40 },
807
- ];
808
- const jobsPerAttempt = [
809
- [],
810
- [],
811
- [{ value: "job-1", label: "AI应用开发工程师(2026) _ 杭州", active: false }],
812
- ];
813
- let ensureCallCount = 0;
814
- let listJobsCallCount = 0;
815
- const page = {
816
- async ensureOnChatPage() {
817
- const next = pageStates[Math.min(ensureCallCount, pageStates.length - 1)];
818
- ensureCallCount += 1;
819
- return next;
820
- },
821
- async listJobs() {
822
- const next = jobsPerAttempt[Math.min(listJobsCallCount, jobsPerAttempt.length - 1)];
823
- listJobsCallCount += 1;
824
- return next;
825
- },
826
- };
827
-
828
- const hydrated = await vendorCliTestables.waitForChatShellHydration({
829
- page,
830
- maxAttempts: 4,
831
- delayMs: 0,
832
- });
833
- assert.equal(Array.isArray(hydrated.jobs), true);
834
- assert.equal(hydrated.jobs.length, 1);
835
- assert.equal(hydrated.pageState.listItemCount, 40);
836
- assert.equal(ensureCallCount >= 3, true);
837
- }
838
-
839
- async function testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile() {
840
- const page = {
841
- _attempt: 0,
842
- async ensureOnChatPage() {
843
- return {
844
- href: "https://www.zhipin.com/web/chat/index",
845
- hasListContainer: this._attempt >= 1,
846
- listItemCount: this._attempt >= 1 ? 10 : 0,
847
- };
848
- },
849
- async listJobs() {
850
- this._attempt += 1;
851
- if (this._attempt < 2) {
852
- return [];
853
- }
854
- return [
855
- {
856
- value: "job-1",
857
- label: "AI应用开发工程师(2026) _ 杭州",
858
- active: false,
859
- },
860
- ];
861
- },
862
- };
863
-
864
- const profile = await vendorCliTestables.promptRunProfile({
865
- page,
866
- persistentProfile: {
867
- llm: {
868
- baseUrl: "https://api.example.com/v1",
869
- apiKey: "sk-test-key",
870
- model: "gpt-4.1-mini",
871
- },
872
- chrome: {
873
- port: 9222,
874
- },
875
- runtime: {},
876
- },
877
- overrides: {
878
- jobSelection: "AI应用开发工程师(2026) _ 杭州",
879
- startFrom: "unread",
880
- screeningCriteria: "小样本联通性验证",
881
- targetCount: 1,
882
- },
883
- });
884
- assert.equal(profile.jobSelection.label, "AI应用开发工程师(2026) _ 杭州");
885
- assert.equal(profile.startFrom, "unread");
886
- assert.equal(profile.targetCount, 1);
887
- }
888
-
889
- function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
890
- const installedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
891
- packageVersion: "1.3.25",
892
- packageRootPath: "C:\\Users\\yaolin\\AppData\\Roaming\\npm\\node_modules\\@reconcrap\\boss-recommend-mcp",
893
- });
894
- assert.equal(installedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
895
-
896
- const cachedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
897
- packageVersion: "1.3.25",
898
- packageRootPath: "C:\\Users\\yaolin\\AppData\\Local\\npm-cache\\_npx\\abcd1234\\node_modules\\@reconcrap\\boss-recommend-mcp",
899
- });
900
- assert.equal(cachedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
901
-
902
- const sourceSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
903
- packageVersion: "1.3.25-dev",
904
- packageRootPath: "C:\\Users\\yaolin\\Documents\\codex_projects\\boss recommend pipeline\\boss-recommend-mcp",
905
- });
906
- assert.equal(sourceSpecifier, "@reconcrap/boss-recommend-mcp@latest");
907
-
908
- const launchConfig = cliTestables.buildMcpLaunchConfig({});
909
- assert.equal(launchConfig.command, "npx");
910
- assert.equal(Array.isArray(launchConfig.args), true);
911
- assert.equal(launchConfig.args[0], "-y");
912
- }
913
-
914
- function testVendorBossChatCliShouldParseSharedLlmTransportArgs() {
915
- const parsed = vendorCliTestables.parseArgs([
916
- "start-run",
917
- "--llm-timeout-ms",
918
- "70000",
919
- "--llm-max-retries",
920
- "5",
921
- ]);
922
- assert.equal(parsed.command, "start-run");
923
- assert.equal(parsed.overrides.llm.timeoutMs, 70000);
924
- assert.equal(parsed.overrides.llm.maxRetries, 5);
925
- }
926
-
927
- function testBossChatLlmParserShouldAcceptMinimalDecisionJson() {
928
- const parsed = parseLlmJson(
929
- JSON.stringify({
930
- passed: true,
931
- }),
932
- );
933
- assert.equal(parsed.passed, true);
934
- assert.equal(parsed.rawOutputText.includes('"passed":true'), true);
935
- }
936
-
937
- function testBossChatLlmParserShouldAcceptPlainPassFailText() {
938
- const passed = parseLlmJson("PASS");
939
- assert.equal(passed.passed, true);
940
- const failed = parseLlmJson("false");
941
- assert.equal(failed.passed, false);
942
- }
943
-
944
- function testBossChatLlmParserShouldAcceptDecisionField() {
945
- const parsed = parseLlmJson(
946
- JSON.stringify({
947
- decision: "fail",
948
- }),
949
- );
950
- assert.equal(parsed.passed, false);
951
- }
952
-
953
- function testBossChatLlmParserShouldPreserveReasoningFields() {
954
- const parsed = parseLlmJson(
955
- JSON.stringify({
956
- passed: true,
957
- reason: "候选人具备 2 段 AI Agent 项目经验",
958
- summary: "符合筛选要求",
959
- evidence: ["AI Agent", "MCP"],
960
- }),
961
- {
962
- reasoningText: "先检查项目经历,再核对技能栈,结论为通过。",
963
- },
964
- );
965
- assert.equal(parsed.passed, true);
966
- assert.equal(parsed.reason, "候选人具备 2 段 AI Agent 项目经验");
967
- assert.equal(parsed.summary, "符合筛选要求");
968
- assert.equal(parsed.cot, "先检查项目经历,再核对技能栈,结论为通过。");
969
- assert.deepEqual(parsed.evidence, ["AI Agent", "MCP"]);
970
- assert.equal(parsed.rawReasoningText, "先检查项目经历,再核对技能栈,结论为通过。");
971
- }
972
-
973
- function testBossChatLlmExtractorsShouldReadProviderReasoningFields() {
974
- const completionReasoning = extractCompletionReasoningText({
975
- choices: [
976
- {
977
- message: {
978
- content: [{ type: "text", text: "{\"passed\":true}" }],
979
- reasoning_content: [{ text: "先核对教育背景,再核对项目经历。" }],
980
- },
981
- },
982
- ],
983
- });
984
- assert.equal(completionReasoning.includes("教育背景"), true);
985
-
986
- const responsesReasoning = extractResponsesReasoningText({
987
- output: [
988
- {
989
- type: "reasoning",
990
- summary: [{ text: "根据项目经历与技能关键词判断为通过。" }],
991
- },
992
- ],
993
- });
994
- assert.equal(responsesReasoning.includes("技能关键词"), true);
995
- }
996
-
997
- async function testBossChatLlmTextChunkFallbackShouldWork() {
998
- const originalChunkSize = process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
999
- const originalChunkOverlap = process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
1000
- const originalMaxChunks = process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
1001
- process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = "1000";
1002
- process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = "120";
1003
- process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = "6";
1004
- try {
751
+ await withBossChatWorkspace(async (workspaceRoot) => {
752
+ const toolsResponse = await handleRequest({
753
+ jsonrpc: "2.0",
754
+ id: 10,
755
+ method: "tools/list",
756
+ params: {}
757
+ }, workspaceRoot);
758
+ const tools = toolsResponse.result.tools;
759
+ const prepareToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_PREPARE_RUN).inputSchema;
760
+ const startToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_START_RUN).inputSchema;
761
+ assert.equal(prepareToolSchema.required, undefined);
762
+ assert.deepEqual(startToolSchema.required, ["job", "start_from", "criteria"]);
763
+ assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("target_count")), true);
764
+ assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("targetCount")), true);
765
+ assert.equal(startToolSchema.properties.target_count.examples.includes("all"), true);
766
+ assert.equal(startToolSchema.examples.some((item) => item.target_count === "all"), true);
767
+
768
+ const prepared = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PREPARE_RUN, {}, 101);
769
+ assert.equal(prepared.status, "NEED_INPUT");
770
+ assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
771
+ const preparedTargetCountQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
772
+ assert.equal(preparedTargetCountQuestion.argument_name, "target_count");
773
+ assert.equal(preparedTargetCountQuestion.recommended_argument_patch.target_count, "all");
774
+
775
+ const needInput = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {}, 11);
776
+ assert.equal(needInput.status, "NEED_INPUT");
777
+ assert.deepEqual(needInput.required_fields, ["job", "start_from", "target_count", "criteria"]);
778
+ assert.equal(Array.isArray(needInput.job_options), true);
779
+ assert.equal(needInput.job_options.length, 2);
780
+ const targetQuestion = needInput.pending_questions.find((item) => item.field === "target_count");
781
+ assert.equal(Boolean(targetQuestion), true);
782
+ assert.equal(targetQuestion.argument_name, "target_count");
783
+ assert.equal(targetQuestion.recommended_argument_patch.target_count, "all");
784
+ assert.equal(targetQuestion.options.some((item) => item.value === "all"), true);
785
+ assert.equal(targetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
786
+
787
+ const missingTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
788
+ job: "算法工程师",
789
+ start_from: "all",
790
+ criteria: "全部候选人都过一遍"
791
+ }, 111);
792
+ assert.equal(missingTargetOnly.status, "NEED_INPUT");
793
+ assert.deepEqual(missingTargetOnly.missing_fields, ["target_count"]);
794
+ assert.equal(missingTargetOnly.next_call_example.target_count, "all");
795
+ assert.equal(missingTargetOnly.accepted_examples.includes(-1), true);
796
+
797
+ const invalidTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
798
+ job: "算法工程师",
799
+ start_from: "all",
800
+ criteria: "全部候选人都过一遍",
801
+ target_count: "not a target"
802
+ }, 112);
803
+ assert.equal(invalidTargetOnly.status, "NEED_INPUT");
804
+ assert.deepEqual(invalidTargetOnly.missing_fields, ["target_count"]);
805
+ assert.equal(invalidTargetOnly.received_target_count, "not a target");
806
+ assert.equal(Boolean(invalidTargetOnly.target_count_parse_error), true);
807
+ assert.equal(invalidTargetOnly.next_call_example.target_count, "all");
808
+ assert.equal(invalidTargetOnly.recommended_argument_patch.target_count, "all");
809
+
810
+ const invalidStartResponse = await handleRequest(
811
+ makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
812
+ start_from: "invalid-value"
813
+ }),
814
+ workspaceRoot
815
+ );
816
+ assert.equal(invalidStartResponse.error.code, -32602);
817
+
818
+ const invalidGetResponse = await handleRequest(
819
+ makeToolCall(12, TOOL_BOSS_CHAT_GET_RUN, {}),
820
+ workspaceRoot
821
+ );
822
+ assert.equal(invalidGetResponse.error.code, -32602);
823
+
824
+ const health = await callTool(workspaceRoot, TOOL_BOSS_CHAT_HEALTH_CHECK, {}, 13);
825
+ assert.equal(health.status, "OK");
826
+
827
+ const started = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
828
+ job: "算法工程师",
829
+ start_from: "unread",
830
+ criteria: "有 AI Agent 经验",
831
+ target_count: 2
832
+ }, 14);
833
+ assert.equal(started.status, "ACCEPTED");
834
+
835
+ const startedAll = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
836
+ job: "算法工程师",
837
+ start_from: "all",
838
+ criteria: "全部候选人都过一遍",
839
+ target_count: "全部候选人"
840
+ }, 140);
841
+ assert.equal(startedAll.status, "ACCEPTED");
842
+ const stateAfterStartAll = readStubState(workspaceRoot);
843
+ assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
844
+
845
+ const startedCamelCase = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
846
+ job: "算法工程师",
847
+ start_from: "all",
848
+ criteria: "全部候选人都过一遍",
849
+ targetCount: "all"
850
+ }, 141);
851
+ assert.equal(startedCamelCase.status, "ACCEPTED");
852
+ assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
853
+
854
+ const running = await callTool(workspaceRoot, TOOL_BOSS_CHAT_GET_RUN, {
855
+ run_id: started.run_id,
856
+ profile: "default"
857
+ }, 15);
858
+ assert.equal(running.run.state, "running");
859
+
860
+ const paused = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PAUSE_RUN, {
861
+ run_id: started.run_id,
862
+ profile: "default"
863
+ }, 16);
864
+ assert.equal(paused.run.state, "paused");
865
+
866
+ const resumed = await callTool(workspaceRoot, TOOL_BOSS_CHAT_RESUME_RUN, {
867
+ run_id: started.run_id,
868
+ profile: "default"
869
+ }, 17);
870
+ assert.equal(resumed.run.state, "running");
871
+
872
+ const canceled = await callTool(workspaceRoot, TOOL_BOSS_CHAT_CANCEL_RUN, {
873
+ run_id: started.run_id,
874
+ profile: "default"
875
+ }, 18);
876
+ assert.equal(canceled.run.state, "canceled");
877
+ });
878
+ }
879
+
880
+ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
881
+ const followUpJson = cliTestables.getRunFollowUp({
882
+ "follow-up-json": JSON.stringify({
883
+ chat: {
884
+ criteria: "有 AI Agent 经验",
885
+ start_from: "unread",
886
+ target_count: 2
887
+ }
888
+ })
889
+ });
890
+ assert.equal(followUpJson.chat.criteria, "有 AI Agent 经验");
891
+ assert.equal(followUpJson.chat.target_count, 2);
892
+
893
+ const tempFile = path.join(os.tmpdir(), `boss-recommend-follow-up-${Date.now()}.json`);
894
+ fs.writeFileSync(tempFile, JSON.stringify({
895
+ chat: {
896
+ criteria: "熟悉 MCP",
897
+ start_from: "all",
898
+ target_count: 3
899
+ }
900
+ }, null, 2));
901
+ try {
902
+ const followUpFile = cliTestables.getRunFollowUp({
903
+ "follow-up-file": tempFile
904
+ });
905
+ assert.equal(followUpFile.chat.criteria, "熟悉 MCP");
906
+ assert.equal(followUpFile.chat.start_from, "all");
907
+ } finally {
908
+ fs.rmSync(tempFile, { force: true });
909
+ }
910
+
911
+ await withBossChatWorkspace(async (workspaceRoot) => {
912
+ const prepareLogs = await captureConsoleLogs(async () => {
913
+ await cliTestables.runBossChatCliCommand("prepare-run", {
914
+ "workspace-root": workspaceRoot
915
+ });
916
+ });
917
+ const prepared = JSON.parse(prepareLogs[0]);
918
+ assert.equal(prepared.status, "NEED_INPUT");
919
+ assert.equal(prepared.pending_questions.find((item) => item.field === "target_count").argument_name, "target_count");
920
+
921
+ const logs = await captureConsoleLogs(async () => {
922
+ await cliTestables.runBossChatCliCommand("run", {
923
+ "workspace-root": workspaceRoot,
924
+ job: "算法工程师",
925
+ "start-from": "unread",
926
+ criteria: "有 AI Agent 经验",
927
+ targetCount: "2"
928
+ });
929
+ });
930
+ assert.equal(logs.length > 0, true);
931
+ const payload = JSON.parse(logs[0]);
932
+ assert.equal(payload.status, "ACCEPTED");
933
+ assert.equal(typeof payload.run_id, "string");
934
+ const state = readStubState(workspaceRoot);
935
+ assert.equal(state.get_calls[payload.run_id] || 0, 0);
936
+
937
+ await captureConsoleLogs(async () => {
938
+ await cliTestables.runBossChatCliCommand("run", {
939
+ "workspace-root": workspaceRoot,
940
+ job: "算法工程师",
941
+ "start-from": "all",
942
+ criteria: "全部候选人都过一遍",
943
+ targetCount: "全部候选人"
944
+ });
945
+ });
946
+ const allState = readStubState(workspaceRoot);
947
+ assert.equal(allState.last_start_args.targetCount, "-1");
948
+ });
949
+ }
950
+
951
+ async function testVendorBossChatCliShouldWaitForHydratedChatShell() {
952
+ const pageStates = [
953
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
954
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
955
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: true, listItemCount: 40 },
956
+ ];
957
+ const jobsPerAttempt = [
958
+ [],
959
+ [],
960
+ [{ value: "job-1", label: "AI应用开发工程师(2026) _ 杭州", active: false }],
961
+ ];
962
+ let ensureCallCount = 0;
963
+ let listJobsCallCount = 0;
964
+ const page = {
965
+ async ensureOnChatPage() {
966
+ const next = pageStates[Math.min(ensureCallCount, pageStates.length - 1)];
967
+ ensureCallCount += 1;
968
+ return next;
969
+ },
970
+ async listJobs() {
971
+ const next = jobsPerAttempt[Math.min(listJobsCallCount, jobsPerAttempt.length - 1)];
972
+ listJobsCallCount += 1;
973
+ return next;
974
+ },
975
+ };
976
+
977
+ const hydrated = await vendorCliTestables.waitForChatShellHydration({
978
+ page,
979
+ maxAttempts: 4,
980
+ delayMs: 0,
981
+ });
982
+ assert.equal(Array.isArray(hydrated.jobs), true);
983
+ assert.equal(hydrated.jobs.length, 1);
984
+ assert.equal(hydrated.pageState.listItemCount, 40);
985
+ assert.equal(ensureCallCount >= 3, true);
986
+ }
987
+
988
+ async function testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile() {
989
+ const page = {
990
+ _attempt: 0,
991
+ async ensureOnChatPage() {
992
+ return {
993
+ href: "https://www.zhipin.com/web/chat/index",
994
+ hasListContainer: this._attempt >= 1,
995
+ listItemCount: this._attempt >= 1 ? 10 : 0,
996
+ };
997
+ },
998
+ async listJobs() {
999
+ this._attempt += 1;
1000
+ if (this._attempt < 2) {
1001
+ return [];
1002
+ }
1003
+ return [
1004
+ {
1005
+ value: "job-1",
1006
+ label: "AI应用开发工程师(2026) _ 杭州",
1007
+ active: false,
1008
+ },
1009
+ ];
1010
+ },
1011
+ };
1012
+
1013
+ const profile = await vendorCliTestables.promptRunProfile({
1014
+ page,
1015
+ persistentProfile: {
1016
+ llm: {
1017
+ baseUrl: "https://api.example.com/v1",
1018
+ apiKey: "sk-test-key",
1019
+ model: "gpt-4.1-mini",
1020
+ },
1021
+ chrome: {
1022
+ port: 9222,
1023
+ },
1024
+ runtime: {},
1025
+ },
1026
+ overrides: {
1027
+ jobSelection: "AI应用开发工程师(2026) _ 杭州",
1028
+ startFrom: "unread",
1029
+ screeningCriteria: "小样本联通性验证",
1030
+ targetCount: 1,
1031
+ },
1032
+ });
1033
+ assert.equal(profile.jobSelection.label, "AI应用开发工程师(2026) _ 杭州");
1034
+ assert.equal(profile.startFrom, "unread");
1035
+ assert.equal(profile.targetCount, 1);
1036
+ }
1037
+
1038
+ function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
1039
+ const installedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
1040
+ packageVersion: "1.3.25",
1041
+ packageRootPath: "C:\\Users\\yaolin\\AppData\\Roaming\\npm\\node_modules\\@reconcrap\\boss-recommend-mcp",
1042
+ });
1043
+ assert.equal(installedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
1044
+
1045
+ const cachedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
1046
+ packageVersion: "1.3.25",
1047
+ packageRootPath: "C:\\Users\\yaolin\\AppData\\Local\\npm-cache\\_npx\\abcd1234\\node_modules\\@reconcrap\\boss-recommend-mcp",
1048
+ });
1049
+ assert.equal(cachedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
1050
+
1051
+ const sourceSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
1052
+ packageVersion: "1.3.25-dev",
1053
+ packageRootPath: "C:\\Users\\yaolin\\Documents\\codex_projects\\boss recommend pipeline\\boss-recommend-mcp",
1054
+ });
1055
+ assert.equal(sourceSpecifier, "@reconcrap/boss-recommend-mcp@latest");
1056
+
1057
+ const launchConfig = cliTestables.buildMcpLaunchConfig({});
1058
+ assert.equal(launchConfig.command, "npx");
1059
+ assert.equal(Array.isArray(launchConfig.args), true);
1060
+ assert.equal(launchConfig.args[0], "-y");
1061
+ }
1062
+
1063
+ function testVendorBossChatCliShouldParseSharedLlmTransportArgs() {
1064
+ const parsed = vendorCliTestables.parseArgs([
1065
+ "start-run",
1066
+ "--llm-timeout-ms",
1067
+ "70000",
1068
+ "--llm-max-retries",
1069
+ "5",
1070
+ ]);
1071
+ assert.equal(parsed.command, "start-run");
1072
+ assert.equal(parsed.overrides.llm.timeoutMs, 70000);
1073
+ assert.equal(parsed.overrides.llm.maxRetries, 5);
1074
+ }
1075
+
1076
+ function testBossChatLlmParserShouldAcceptMinimalDecisionJson() {
1077
+ const parsed = parseLlmJson(
1078
+ JSON.stringify({
1079
+ passed: true,
1080
+ }),
1081
+ );
1082
+ assert.equal(parsed.passed, true);
1083
+ assert.equal(parsed.rawOutputText.includes('"passed":true'), true);
1084
+ }
1085
+
1086
+ function testBossChatLlmParserShouldAcceptPlainPassFailText() {
1087
+ const passed = parseLlmJson("PASS");
1088
+ assert.equal(passed.passed, true);
1089
+ const failed = parseLlmJson("false");
1090
+ assert.equal(failed.passed, false);
1091
+ }
1092
+
1093
+ function testBossChatLlmParserShouldAcceptDecisionField() {
1094
+ const parsed = parseLlmJson(
1095
+ JSON.stringify({
1096
+ decision: "fail",
1097
+ }),
1098
+ );
1099
+ assert.equal(parsed.passed, false);
1100
+ }
1101
+
1102
+ function testBossChatLlmParserShouldPreserveReasoningFields() {
1103
+ const parsed = parseLlmJson(
1104
+ JSON.stringify({
1105
+ passed: true,
1106
+ reason: "候选人具备 2 段 AI Agent 项目经验",
1107
+ summary: "符合筛选要求",
1108
+ evidence: ["AI Agent", "MCP"],
1109
+ }),
1110
+ {
1111
+ reasoningText: "先检查项目经历,再核对技能栈,结论为通过。",
1112
+ },
1113
+ );
1114
+ assert.equal(parsed.passed, true);
1115
+ assert.equal(parsed.reason, "候选人具备 2 段 AI Agent 项目经验");
1116
+ assert.equal(parsed.summary, "符合筛选要求");
1117
+ assert.equal(parsed.cot, "先检查项目经历,再核对技能栈,结论为通过。");
1118
+ assert.deepEqual(parsed.evidence, ["AI Agent", "MCP"]);
1119
+ assert.equal(parsed.rawReasoningText, "先检查项目经历,再核对技能栈,结论为通过。");
1120
+ }
1121
+
1122
+ function testBossChatLlmExtractorsShouldReadProviderReasoningFields() {
1123
+ const completionReasoning = extractCompletionReasoningText({
1124
+ choices: [
1125
+ {
1126
+ message: {
1127
+ content: [{ type: "text", text: "{\"passed\":true}" }],
1128
+ reasoning_content: [{ text: "先核对教育背景,再核对项目经历。" }],
1129
+ },
1130
+ },
1131
+ ],
1132
+ });
1133
+ assert.equal(completionReasoning.includes("教育背景"), true);
1134
+
1135
+ const responsesReasoning = extractResponsesReasoningText({
1136
+ output: [
1137
+ {
1138
+ type: "reasoning",
1139
+ summary: [{ text: "根据项目经历与技能关键词判断为通过。" }],
1140
+ },
1141
+ ],
1142
+ });
1143
+ assert.equal(responsesReasoning.includes("技能关键词"), true);
1144
+ }
1145
+
1146
+ async function testBossChatLlmTextChunkFallbackShouldWork() {
1147
+ const originalChunkSize = process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
1148
+ const originalChunkOverlap = process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
1149
+ const originalMaxChunks = process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
1150
+ process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = "1000";
1151
+ process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = "120";
1152
+ process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = "6";
1153
+ try {
1005
1154
  class FakeChunkFallbackClient extends LlmClient {
1006
1155
  constructor() {
1007
1156
  super({
1008
1157
  baseUrl: "https://api.example.com/v1",
1009
- apiKey: "sk-test",
1010
- model: "gpt-test",
1011
- });
1012
- this.calls = [];
1013
- }
1158
+ apiKey: "sk-test",
1159
+ model: "gpt-test",
1160
+ });
1161
+ this.calls = [];
1162
+ }
1014
1163
 
1015
1164
  async requestByPreference(payload) {
1016
1165
  this.calls.push(payload);
1017
- if (payload.chunkTotal === 1 && !payload.imageDataUrl) {
1166
+ const prompt = String(payload?.prompt || "");
1167
+ if (prompt.includes("简历文本:") && !prompt.includes("当前分段:")) {
1018
1168
  const error = new Error("maximum context length exceeded");
1019
1169
  throw error;
1020
1170
  }
1021
- if (payload.chunkTotal > 1) {
1022
- if (payload.chunkIndex === 2) {
1023
- return {
1024
- passed: true,
1025
- rawOutputText: '{"passed":true}',
1026
- chunkIndex: payload.chunkIndex,
1027
- chunkTotal: payload.chunkTotal,
1028
- };
1029
- }
1171
+ if (prompt.includes("当前分段:")) {
1030
1172
  return {
1031
- passed: false,
1032
- rawOutputText: '{"passed":false}',
1173
+ rawOutputText: JSON.stringify({
1174
+ chunk_passed: Number(payload.chunkIndex) === 2,
1175
+ chunk_summary: Number(payload.chunkIndex) === 2 ? "命中核心 AI 项目经历" : "当前分段证据不足",
1176
+ hard_evidence: Number(payload.chunkIndex) === 2 ? ["PASS_MARKER_DEF"] : ["PASS_MARKER_ABC"],
1177
+ soft_evidence: [],
1178
+ hard_blockers: [],
1179
+ missing_or_uncertain: Number(payload.chunkIndex) === 2 ? [] : ["需结合后续分段"],
1180
+ quoted_spans: Number(payload.chunkIndex) === 2 ? ["PASS_MARKER_DEF"] : ["PASS_MARKER_ABC"],
1181
+ chunk_index: payload.chunkIndex,
1182
+ chunk_total: payload.chunkTotal,
1183
+ }),
1184
+ chunk_passed: Number(payload.chunkIndex) === 2,
1033
1185
  chunkIndex: payload.chunkIndex,
1034
1186
  chunkTotal: payload.chunkTotal,
1035
1187
  };
1036
1188
  }
1037
1189
  return {
1038
- passed: false,
1039
- rawOutputText: '{"passed":false}',
1040
- chunkIndex: 1,
1041
- chunkTotal: 1,
1190
+ passed: true,
1191
+ rawOutputText: JSON.stringify({
1192
+ passed: true,
1193
+ reason: "综合全部分段后,候选人具备目标 AI 项目证据。",
1194
+ summary: "长简历聚合后通过",
1195
+ evidence: ["PASS_MARKER_DEF"],
1196
+ }),
1197
+ chunkIndex: null,
1198
+ chunkTotal: payload?.aggregateInput?.chunk_count || null,
1042
1199
  };
1043
1200
  }
1044
1201
  }
1045
-
1046
- const client = new FakeChunkFallbackClient();
1047
- const longResume = `${"A".repeat(1200)} PASS_MARKER_ABC ${"B".repeat(1200)} PASS_MARKER_DEF`;
1048
- const result = await client.evaluateResume({
1049
- screeningCriteria: "有 AI 项目经验",
1050
- candidate: {
1051
- name: "候选人A",
1052
- sourceJob: "算法工程师",
1053
- resumeText: longResume,
1054
- evidenceCorpus: longResume,
1055
- },
1056
- imagePath: null,
1202
+
1203
+ const client = new FakeChunkFallbackClient();
1204
+ const longResume = `${"A".repeat(1200)} PASS_MARKER_ABC ${"B".repeat(1200)} PASS_MARKER_DEF`;
1205
+ const result = await client.evaluateResume({
1206
+ screeningCriteria: "有 AI 项目经验",
1207
+ candidate: {
1208
+ name: "候选人A",
1209
+ sourceJob: "算法工程师",
1210
+ resumeText: longResume,
1211
+ evidenceCorpus: longResume,
1212
+ },
1213
+ imagePath: null,
1057
1214
  });
1058
1215
  assert.equal(result.passed, true);
1059
- assert.equal(result.evaluationMode, "text");
1060
- assert.equal(result.chunkIndex, 2);
1216
+ assert.equal(result.evaluationMode, "text-chunk-aggregate");
1217
+ assert.equal(result.chunkIndex, null);
1061
1218
  assert.equal(Number(result.chunkTotal) > 1, true);
1062
1219
  } finally {
1063
- if (originalChunkSize === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
1064
- else process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = originalChunkSize;
1065
- if (originalChunkOverlap === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
1066
- else process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = originalChunkOverlap;
1067
- if (originalMaxChunks === undefined) delete process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
1068
- else process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = originalMaxChunks;
1069
- }
1070
- }
1071
-
1072
- async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
1073
- const completionResponse = {
1074
- ok: true,
1075
- status: 200,
1076
- async json() {
1077
- return {
1078
- choices: [
1079
- {
1080
- message: {
1081
- content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1082
- }
1083
- }
1084
- ]
1085
- };
1086
- }
1087
- };
1088
- const responsesResponse = {
1089
- ok: true,
1090
- status: 200,
1091
- async json() {
1092
- return {
1093
- output_text: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1094
- };
1095
- }
1096
- };
1097
-
1098
- let volcCompletionPayload = null;
1099
- const volcClient = new LlmClient({
1100
- baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
1101
- apiKey: "sk-test",
1102
- model: "doubao-seed-2-0-mini-260215",
1103
- }, {
1104
- fetchImpl: async (_url, options = {}) => {
1105
- volcCompletionPayload = JSON.parse(String(options.body || "{}"));
1106
- return completionResponse;
1107
- },
1108
- });
1109
- await volcClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1110
- assert.deepEqual(volcCompletionPayload.thinking, { type: "enabled" });
1111
- assert.equal(volcCompletionPayload.reasoning_effort, "low");
1112
-
1113
- let lowCompletionPayload = null;
1114
- const lowClient = new LlmClient({
1115
- baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
1116
- apiKey: "sk-test",
1117
- model: "doubao-seed-2-0-mini-260215",
1118
- thinkingLevel: "low",
1119
- }, {
1120
- fetchImpl: async (_url, options = {}) => {
1121
- lowCompletionPayload = JSON.parse(String(options.body || "{}"));
1122
- return completionResponse;
1123
- },
1124
- });
1125
- await lowClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1126
- assert.deepEqual(lowCompletionPayload.thinking, { type: "enabled" });
1127
- assert.equal(lowCompletionPayload.reasoning_effort, "low");
1128
-
1129
- let openaiCompletionPayload = null;
1130
- const openaiClient = new LlmClient({
1131
- baseUrl: "https://api.openai.com/v1",
1132
- apiKey: "sk-test",
1133
- model: "gpt-test",
1134
- }, {
1135
- fetchImpl: async (_url, options = {}) => {
1136
- openaiCompletionPayload = JSON.parse(String(options.body || "{}"));
1137
- return completionResponse;
1138
- },
1139
- });
1140
- await openaiClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1141
- assert.equal(openaiCompletionPayload.thinking, undefined);
1142
- assert.equal(openaiCompletionPayload.reasoning_effort, "low");
1143
-
1144
- let responsesPayload = null;
1145
- const responsesClient = new LlmClient({
1146
- baseUrl: "https://api.openai.com/v1",
1147
- apiKey: "sk-test",
1148
- model: "gpt-test",
1149
- thinkingLevel: "low",
1150
- }, {
1151
- fetchImpl: async (_url, options = {}) => {
1152
- responsesPayload = JSON.parse(String(options.body || "{}"));
1153
- return responsesResponse;
1154
- },
1155
- });
1156
- await responsesClient.requestResponses({ prompt: "prompt", evidenceCorpus: "resume" });
1157
- assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
1158
- }
1159
-
1160
- async function testBossChatLlmShouldSendAllImageChunksInSingleRequest() {
1161
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-image-chunks-"));
1162
- const firstImage = path.join(tempDir, "chunk-1.png");
1163
- const secondImage = path.join(tempDir, "chunk-2.png");
1164
- fs.writeFileSync(
1165
- firstImage,
1166
- Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1167
- );
1168
- fs.writeFileSync(
1169
- secondImage,
1170
- Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1171
- );
1172
-
1173
- let completionPayload = null;
1174
- const client = new LlmClient({
1175
- baseUrl: "https://api.openai.com/v1",
1176
- apiKey: "sk-test",
1177
- model: "gpt-test",
1178
- }, {
1179
- fetchImpl: async (_url, options = {}) => {
1180
- completionPayload = JSON.parse(String(options.body || "{}"));
1181
- return {
1182
- ok: true,
1183
- status: 200,
1184
- async json() {
1185
- return {
1186
- choices: [
1187
- {
1188
- message: {
1189
- content: "{\"passed\":true}",
1190
- },
1191
- },
1192
- ],
1193
- };
1194
- },
1195
- };
1196
- },
1197
- });
1198
-
1199
- try {
1200
- const result = await client.evaluateResume({
1201
- screeningCriteria: "有 AI 项目经验",
1202
- candidate: {
1203
- name: "候选人A",
1204
- sourceJob: "算法工程师",
1205
- resumeText: "",
1206
- evidenceCorpus: "",
1207
- },
1208
- imagePaths: [firstImage, secondImage],
1209
- });
1210
-
1211
- assert.equal(result.passed, true);
1212
- assert.equal(result.evaluationMode, "image-multi-chunk");
1213
- assert.equal(result.imageCount, 2);
1214
- assert.equal(Array.isArray(completionPayload.messages?.[0]?.content), true);
1215
- assert.equal(
1216
- completionPayload.messages[0].content.filter((item) => item.type === "image_url").length,
1217
- 2,
1218
- );
1219
- } finally {
1220
- fs.rmSync(tempDir, { recursive: true, force: true });
1221
- }
1222
- }
1223
-
1224
- async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1225
- const calls = [];
1226
- const page = {
1227
- async ensureReady() {
1228
- calls.push("ensureReady");
1229
- return { hasListContainer: true, listItemCount: 1 };
1230
- },
1231
- async activatePrimaryChatLabel(label) {
1232
- calls.push(`activatePrimaryChatLabel:${label}`);
1233
- return { changed: false, verified: true, activeLabel: label };
1234
- },
1235
- async selectJob(jobSelection) {
1236
- calls.push(`selectJob:${jobSelection.label}`);
1237
- return jobSelection;
1238
- },
1239
- async activateUnreadFilter() {
1240
- calls.push("activateUnreadFilter");
1241
- return { changed: false, verified: true, activeLabel: "未读" };
1242
- },
1243
- async primeConversationByFirstCandidate() {
1244
- calls.push("primeConversationByFirstCandidate:1");
1245
- return {
1246
- candidate: {
1247
- customerId: "1001",
1248
- name: "候选人A",
1249
- sourceJob: "算法工程师",
1250
- domIndex: 0,
1251
- },
1252
- totalVisibleCandidates: 1,
1253
- readyState: {
1254
- hasOnlineResume: true,
1255
- hasAskResume: true,
1256
- hasAttachmentResume: false,
1257
- },
1258
- };
1259
- },
1260
- async getLoadedCustomers() {
1261
- calls.push("getLoadedCustomers:1");
1262
- return [];
1263
- },
1264
- async closeResumeModalDomOnce() {
1265
- return {
1266
- closed: true,
1267
- method: "already-closed",
1268
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1269
- };
1270
- },
1271
- };
1272
- const stateStore = {
1273
- async load() {},
1274
- hasAny() {
1275
- return false;
1276
- },
1277
- async record() {},
1278
- };
1279
- const app = new BossChatApp({
1280
- page,
1281
- llmClient: {},
1282
- interaction: {
1283
- async sleepRange() {},
1284
- async maybeRest() {},
1285
- },
1286
- resumeCaptureService: {},
1287
- stateStore,
1288
- reportStore: {
1289
- async write() {
1290
- return "report.json";
1291
- },
1292
- },
1293
- logger: { log() {} },
1294
- dryRun: true,
1295
- artifactRootDir: os.tmpdir(),
1296
- resumeOpenCooldownMs: 0,
1297
- });
1298
- app.waitForCandidateList = async ({ reason } = {}) => {
1299
- calls.push(`waitForCandidateList:${reason || "unknown"}`);
1300
- return {
1301
- ready: true,
1302
- waitedMs: 0,
1303
- attempts: 1,
1304
- listItemCount: 1,
1305
- lastError: "",
1306
- };
1307
- };
1308
- app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1309
- calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1310
- return {
1311
- name: "候选人A",
1312
- passed: false,
1313
- requested: false,
1314
- reason: "skip",
1315
- error: "",
1316
- artifacts: {},
1317
- };
1318
- };
1319
-
1320
- const summary = await app.run({
1321
- screeningCriteria: "有 AI 项目经验",
1322
- targetCount: 1,
1323
- startFrom: "unread",
1324
- jobSelection: { label: "算法工程师", value: "job-1" },
1325
- chrome: { port: 9222 },
1326
- llm: { model: "gpt-test" },
1327
- });
1328
-
1329
- assert.deepEqual(calls.slice(0, 4), [
1330
- "ensureReady",
1331
- "activatePrimaryChatLabel:全部",
1332
- "selectJob:算法工程师",
1333
- "activateUnreadFilter",
1334
- ]);
1335
- assert.equal(calls.includes("primeConversationByFirstCandidate:1"), true);
1336
- assert.equal(calls.includes("processCustomer:skip"), true);
1337
- assert.equal(summary.inspected, 1);
1338
- assert.equal(summary.skipped, 1);
1339
- }
1340
-
1341
- async function testBossChatAppShouldCloseCandidateDetailDuringRunCleanup() {
1342
- const calls = [];
1343
- const page = {
1344
- async ensureReady() {
1345
- calls.push("ensureReady");
1346
- return { hasListContainer: true, listItemCount: 1 };
1347
- },
1348
- async activatePrimaryChatLabel(label) {
1349
- calls.push(`activatePrimaryChatLabel:${label}`);
1350
- return { changed: false, verified: true, activeLabel: label };
1351
- },
1352
- async selectJob(jobSelection) {
1353
- calls.push(`selectJob:${jobSelection.label}`);
1354
- return jobSelection;
1355
- },
1356
- async activateUnreadFilter() {
1357
- calls.push("activateUnreadFilter");
1358
- return { changed: false, verified: true, activeLabel: "未读" };
1359
- },
1360
- async primeConversationByFirstCandidate() {
1361
- calls.push("primeConversationByFirstCandidate:1");
1362
- return {
1363
- candidate: {
1364
- customerId: "1008",
1365
- name: "候选人清理",
1366
- sourceJob: "算法工程师",
1367
- domIndex: 0
1368
- },
1369
- totalVisibleCandidates: 1,
1370
- readyState: {
1371
- hasOnlineResume: true,
1372
- hasAskResume: true,
1373
- hasAttachmentResume: false
1374
- }
1375
- };
1376
- },
1377
- async getLoadedCustomers() {
1378
- calls.push("getLoadedCustomers:1");
1379
- return [];
1380
- },
1381
- async closeResumeModalDomOnce() {
1382
- calls.push("closeResumeModalDomOnce");
1383
- return {
1384
- closed: true,
1385
- method: "already-closed",
1386
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" }
1387
- };
1388
- },
1389
- async closeCandidateDetailDomOnce() {
1390
- calls.push("closeCandidateDetailDomOnce");
1391
- return {
1392
- closed: true,
1393
- method: "dom-close-once:.close-btn",
1394
- finalState: { panelCount: 0, closeCount: 0, topPanelClass: "" }
1395
- };
1396
- }
1397
- };
1398
- const stateStore = {
1399
- async load() {},
1400
- hasAny() {
1401
- return false;
1402
- },
1403
- async record() {}
1404
- };
1405
- const app = new BossChatApp({
1406
- page,
1407
- llmClient: {},
1408
- interaction: {
1409
- async sleepRange() {},
1410
- async maybeRest() {}
1411
- },
1412
- resumeCaptureService: {},
1413
- stateStore,
1414
- reportStore: {
1415
- async write() {
1416
- return "report.json";
1417
- }
1418
- },
1419
- logger: { log() {} },
1420
- dryRun: true,
1421
- artifactRootDir: os.tmpdir(),
1422
- resumeOpenCooldownMs: 0
1423
- });
1424
- app.waitForCandidateList = async ({ reason } = {}) => {
1425
- calls.push(`waitForCandidateList:${reason || "unknown"}`);
1426
- return {
1427
- ready: true,
1428
- waitedMs: 0,
1429
- attempts: 1,
1430
- listItemCount: 1,
1431
- lastError: ""
1432
- };
1433
- };
1434
- app.processCustomer = async () => ({
1435
- name: "候选人清理",
1436
- passed: false,
1437
- requested: false,
1438
- reason: "skip",
1439
- error: "",
1440
- artifacts: {}
1441
- });
1442
-
1443
- const summary = await app.run({
1444
- screeningCriteria: "有 AI 项目经验",
1445
- targetCount: 1,
1446
- startFrom: "unread",
1447
- jobSelection: { label: "算法工程师", value: "job-1" },
1448
- chrome: { port: 9222 },
1449
- llm: { model: "gpt-test" }
1450
- });
1451
-
1452
- assert.equal(summary.inspected, 1);
1453
- assert.equal(calls.includes("closeCandidateDetailDomOnce"), true);
1454
- assert.equal(calls.lastIndexOf("closeCandidateDetailDomOnce") > calls.indexOf("getLoadedCustomers:1"), true);
1455
- }
1456
-
1457
- async function testBossChatAppShouldRestoreListContextAfterRecovery() {
1458
- const calls = [];
1459
- let primeCount = 0;
1460
- let loadedCount = 0;
1461
- const page = {
1462
- async ensureReady() {
1463
- return { hasListContainer: true, listItemCount: 1 };
1464
- },
1465
- async activatePrimaryChatLabel(label) {
1466
- calls.push(`activatePrimaryChatLabel:${label}`);
1467
- return { changed: false, verified: true, activeLabel: label };
1468
- },
1469
- async selectJob(jobSelection) {
1470
- calls.push(`selectJob:${jobSelection.label}`);
1471
- return jobSelection;
1472
- },
1473
- async activateUnreadFilter() {
1474
- calls.push("activateUnreadFilter");
1475
- return { changed: false, verified: true, activeLabel: "未读" };
1476
- },
1477
- async primeConversationByFirstCandidate() {
1478
- primeCount += 1;
1479
- calls.push(`primeConversationByFirstCandidate:${primeCount}`);
1480
- if (primeCount === 1) {
1481
- throw new Error("NO_FIRST_CANDIDATE");
1482
- }
1483
- return {
1484
- candidate: {
1485
- customerId: "1002",
1486
- name: "候选人B",
1487
- sourceJob: "算法工程师",
1488
- domIndex: 0,
1489
- },
1490
- totalVisibleCandidates: 1,
1491
- readyState: {
1492
- hasOnlineResume: true,
1493
- hasAskResume: true,
1494
- hasAttachmentResume: false,
1495
- },
1496
- };
1497
- },
1498
- async getLoadedCustomers() {
1499
- loadedCount += 1;
1500
- calls.push(`getLoadedCustomers:${loadedCount}`);
1501
- if (loadedCount === 1) {
1502
- throw new Error("CHAT_CARD_LIST_NOT_FOUND");
1503
- }
1504
- return [];
1505
- },
1506
- async recoverToChatIndex() {
1507
- calls.push("recoverToChatIndex");
1508
- return { changed: true, href: "https://www.zhipin.com/web/chat/index" };
1509
- },
1510
- async closeResumeModalDomOnce() {
1511
- return {
1512
- closed: true,
1513
- method: "already-closed",
1514
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1515
- };
1516
- },
1517
- };
1518
- const stateStore = {
1519
- async load() {},
1520
- hasAny() {
1521
- return false;
1522
- },
1523
- async record() {},
1524
- };
1525
- const app = new BossChatApp({
1526
- page,
1527
- llmClient: {},
1528
- interaction: {
1529
- async sleepRange() {},
1530
- async maybeRest() {},
1531
- },
1532
- resumeCaptureService: {},
1533
- stateStore,
1534
- reportStore: {
1535
- async write() {
1536
- return "report.json";
1537
- },
1538
- },
1539
- logger: { log() {} },
1540
- dryRun: true,
1541
- artifactRootDir: os.tmpdir(),
1542
- resumeOpenCooldownMs: 0,
1543
- });
1544
- app.waitForCandidateList = async ({ reason } = {}) => {
1545
- calls.push(`waitForCandidateList:${reason || "unknown"}`);
1546
- return {
1547
- ready:
1548
- reason === "initial-context-restore" ||
1549
- reason === "post-recovery-context-restore",
1550
- waitedMs: 0,
1551
- attempts: 1,
1552
- listItemCount:
1553
- reason === "initial-context-restore" ||
1554
- reason === "post-recovery-context-restore"
1555
- ? 1
1556
- : 0,
1557
- lastError: "",
1558
- };
1559
- };
1560
- app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1561
- calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1562
- return {
1563
- name: "候选人B",
1564
- passed: false,
1565
- requested: false,
1566
- reason: "skip",
1567
- error: "",
1568
- artifacts: {},
1569
- };
1570
- };
1571
-
1572
- const summary = await app.run({
1573
- screeningCriteria: "有 AI 项目经验",
1574
- targetCount: 1,
1575
- startFrom: "unread",
1576
- jobSelection: { label: "算法工程师", value: "job-1" },
1577
- chrome: { port: 9222 },
1578
- llm: { model: "gpt-test" },
1579
- });
1580
-
1581
- assert.equal(calls.filter((item) => item === "activatePrimaryChatLabel:全部").length, 2);
1582
- const recoverIndex = calls.indexOf("recoverToChatIndex");
1583
- assert.equal(recoverIndex >= 0, true);
1584
- assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
1585
- assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
1586
- assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
1587
- assert.equal(calls[recoverIndex + 4], "waitForCandidateList:post-recovery-context-restore");
1588
- assert.equal(calls[recoverIndex + 5], "primeConversationByFirstCandidate:2");
1589
- assert.equal(calls.includes("processCustomer:skip"), true);
1590
- assert.equal(summary.inspected, 1);
1591
- assert.equal(summary.skipped, 1);
1592
- }
1593
-
1594
- async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
1595
- const calls = [];
1596
- let pageStateCall = 0;
1597
- const page = {
1598
- async ensureReady() {
1599
- calls.push("ensureReady");
1600
- return { hasListContainer: false, listItemCount: 0 };
1601
- },
1602
- async activatePrimaryChatLabel(label) {
1603
- calls.push(`activatePrimaryChatLabel:${label}`);
1604
- return { changed: false, verified: true, activeLabel: label };
1605
- },
1606
- async selectJob(jobSelection) {
1607
- calls.push(`selectJob:${jobSelection.label}`);
1608
- return jobSelection;
1609
- },
1610
- async activateUnreadFilter() {
1611
- calls.push("activateUnreadFilter");
1612
- return { changed: true, verified: true, activeLabel: "未读" };
1613
- },
1614
- async getPageState() {
1615
- pageStateCall += 1;
1616
- calls.push(`getPageState:${pageStateCall}`);
1617
- return {
1618
- href: "https://www.zhipin.com/web/chat/index",
1619
- readyState: "complete",
1620
- hasListContainer: pageStateCall >= 3,
1621
- listItemCount: pageStateCall >= 3 ? 2 : 0,
1622
- };
1623
- },
1624
- async primeConversationByFirstCandidate() {
1625
- calls.push("primeConversationByFirstCandidate:1");
1626
- return {
1627
- candidate: {
1628
- customerId: "1003",
1629
- name: "候选人C",
1630
- sourceJob: "算法工程师",
1631
- domIndex: 0,
1632
- },
1633
- totalVisibleCandidates: 2,
1634
- readyState: {
1635
- hasOnlineResume: true,
1636
- hasAskResume: true,
1637
- hasAttachmentResume: false,
1638
- },
1639
- };
1640
- },
1641
- async getLoadedCustomers() {
1642
- calls.push("getLoadedCustomers:1");
1643
- return [];
1644
- },
1645
- async closeResumeModalDomOnce() {
1646
- return {
1647
- closed: true,
1648
- method: "already-closed",
1649
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1650
- };
1651
- },
1652
- };
1653
- const stateStore = {
1654
- async load() {},
1655
- hasAny() {
1656
- return false;
1657
- },
1658
- async record() {},
1659
- };
1660
- const app = new BossChatApp({
1661
- page,
1662
- llmClient: {},
1663
- interaction: {
1664
- async sleepRange() {},
1665
- async maybeRest() {},
1666
- },
1667
- resumeCaptureService: {},
1668
- stateStore,
1669
- reportStore: {
1670
- async write() {
1671
- return "report.json";
1672
- },
1673
- },
1674
- logger: { log() {} },
1675
- dryRun: true,
1676
- artifactRootDir: os.tmpdir(),
1677
- resumeOpenCooldownMs: 0,
1678
- });
1679
- app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1680
- calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1681
- return {
1682
- name: "候选人C",
1683
- passed: false,
1684
- requested: false,
1685
- reason: "skip",
1686
- error: "",
1687
- artifacts: {},
1688
- };
1689
- };
1690
-
1691
- const summary = await app.run({
1692
- screeningCriteria: "有 AI 项目经验",
1693
- targetCount: 1,
1694
- startFrom: "unread",
1695
- jobSelection: { label: "算法工程师", value: "job-1" },
1696
- chrome: { port: 9222 },
1697
- llm: { model: "gpt-test" },
1698
- });
1699
-
1700
- const primeIndex = calls.indexOf("primeConversationByFirstCandidate:1");
1701
- const thirdStateIndex = calls.indexOf("getPageState:3");
1702
- assert.equal(thirdStateIndex >= 0, true);
1703
- assert.equal(primeIndex > thirdStateIndex, true);
1704
- assert.equal(summary.inspected, 1);
1705
- assert.equal(summary.skipped, 1);
1706
- }
1707
-
1220
+ if (originalChunkSize === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
1221
+ else process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = originalChunkSize;
1222
+ if (originalChunkOverlap === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
1223
+ else process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = originalChunkOverlap;
1224
+ if (originalMaxChunks === undefined) delete process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
1225
+ else process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = originalMaxChunks;
1226
+ }
1227
+ }
1228
+
1229
+ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
1230
+ const completionResponse = {
1231
+ ok: true,
1232
+ status: 200,
1233
+ async json() {
1234
+ return {
1235
+ choices: [
1236
+ {
1237
+ message: {
1238
+ content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1239
+ }
1240
+ }
1241
+ ]
1242
+ };
1243
+ }
1244
+ };
1245
+ const responsesResponse = {
1246
+ ok: true,
1247
+ status: 200,
1248
+ async json() {
1249
+ return {
1250
+ output_text: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1251
+ };
1252
+ }
1253
+ };
1254
+
1255
+ let volcCompletionPayload = null;
1256
+ const volcClient = new LlmClient({
1257
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
1258
+ apiKey: "sk-test",
1259
+ model: "doubao-seed-2-0-mini-260215",
1260
+ }, {
1261
+ fetchImpl: async (_url, options = {}) => {
1262
+ volcCompletionPayload = JSON.parse(String(options.body || "{}"));
1263
+ return completionResponse;
1264
+ },
1265
+ });
1266
+ await volcClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1267
+ assert.deepEqual(volcCompletionPayload.thinking, { type: "enabled" });
1268
+ assert.equal(volcCompletionPayload.reasoning_effort, "low");
1269
+
1270
+ let lowCompletionPayload = null;
1271
+ const lowClient = new LlmClient({
1272
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
1273
+ apiKey: "sk-test",
1274
+ model: "doubao-seed-2-0-mini-260215",
1275
+ thinkingLevel: "low",
1276
+ }, {
1277
+ fetchImpl: async (_url, options = {}) => {
1278
+ lowCompletionPayload = JSON.parse(String(options.body || "{}"));
1279
+ return completionResponse;
1280
+ },
1281
+ });
1282
+ await lowClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1283
+ assert.deepEqual(lowCompletionPayload.thinking, { type: "enabled" });
1284
+ assert.equal(lowCompletionPayload.reasoning_effort, "low");
1285
+
1286
+ let openaiCompletionPayload = null;
1287
+ const openaiClient = new LlmClient({
1288
+ baseUrl: "https://api.openai.com/v1",
1289
+ apiKey: "sk-test",
1290
+ model: "gpt-test",
1291
+ }, {
1292
+ fetchImpl: async (_url, options = {}) => {
1293
+ openaiCompletionPayload = JSON.parse(String(options.body || "{}"));
1294
+ return completionResponse;
1295
+ },
1296
+ });
1297
+ await openaiClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
1298
+ assert.equal(openaiCompletionPayload.thinking, undefined);
1299
+ assert.equal(openaiCompletionPayload.reasoning_effort, "low");
1300
+
1301
+ let responsesPayload = null;
1302
+ const responsesClient = new LlmClient({
1303
+ baseUrl: "https://api.openai.com/v1",
1304
+ apiKey: "sk-test",
1305
+ model: "gpt-test",
1306
+ thinkingLevel: "low",
1307
+ }, {
1308
+ fetchImpl: async (_url, options = {}) => {
1309
+ responsesPayload = JSON.parse(String(options.body || "{}"));
1310
+ return responsesResponse;
1311
+ },
1312
+ });
1313
+ await responsesClient.requestResponses({ prompt: "prompt", evidenceCorpus: "resume" });
1314
+ assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
1315
+ }
1316
+
1317
+ async function testBossChatLlmShouldSendAllImageChunksInSingleRequest() {
1318
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-image-chunks-"));
1319
+ const firstImage = path.join(tempDir, "chunk-1.png");
1320
+ const secondImage = path.join(tempDir, "chunk-2.png");
1321
+ fs.writeFileSync(
1322
+ firstImage,
1323
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1324
+ );
1325
+ fs.writeFileSync(
1326
+ secondImage,
1327
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1328
+ );
1329
+
1330
+ let completionPayload = null;
1331
+ const client = new LlmClient({
1332
+ baseUrl: "https://api.openai.com/v1",
1333
+ apiKey: "sk-test",
1334
+ model: "gpt-test",
1335
+ }, {
1336
+ fetchImpl: async (_url, options = {}) => {
1337
+ completionPayload = JSON.parse(String(options.body || "{}"));
1338
+ return {
1339
+ ok: true,
1340
+ status: 200,
1341
+ async json() {
1342
+ return {
1343
+ choices: [
1344
+ {
1345
+ message: {
1346
+ content: "{\"passed\":true}",
1347
+ },
1348
+ },
1349
+ ],
1350
+ };
1351
+ },
1352
+ };
1353
+ },
1354
+ });
1355
+
1356
+ try {
1357
+ const result = await client.evaluateResume({
1358
+ screeningCriteria: "有 AI 项目经验",
1359
+ candidate: {
1360
+ name: "候选人A",
1361
+ sourceJob: "算法工程师",
1362
+ resumeText: "",
1363
+ evidenceCorpus: "",
1364
+ },
1365
+ imagePaths: [firstImage, secondImage],
1366
+ });
1367
+
1368
+ assert.equal(result.passed, true);
1369
+ assert.equal(result.evaluationMode, "image-multi-chunk");
1370
+ assert.equal(result.imageCount, 2);
1371
+ assert.equal(Array.isArray(completionPayload.messages?.[0]?.content), true);
1372
+ assert.equal(
1373
+ completionPayload.messages[0].content.filter((item) => item.type === "image_url").length,
1374
+ 2,
1375
+ );
1376
+ } finally {
1377
+ fs.rmSync(tempDir, { recursive: true, force: true });
1378
+ }
1379
+ }
1380
+
1381
+ async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1382
+ const calls = [];
1383
+ const page = {
1384
+ async ensureReady() {
1385
+ calls.push("ensureReady");
1386
+ return { hasListContainer: true, listItemCount: 1 };
1387
+ },
1388
+ async activatePrimaryChatLabel(label) {
1389
+ calls.push(`activatePrimaryChatLabel:${label}`);
1390
+ return { changed: false, verified: true, activeLabel: label };
1391
+ },
1392
+ async selectJob(jobSelection) {
1393
+ calls.push(`selectJob:${jobSelection.label}`);
1394
+ return jobSelection;
1395
+ },
1396
+ async activateUnreadFilter() {
1397
+ calls.push("activateUnreadFilter");
1398
+ return { changed: false, verified: true, activeLabel: "未读" };
1399
+ },
1400
+ async primeConversationByFirstCandidate() {
1401
+ calls.push("primeConversationByFirstCandidate:1");
1402
+ return {
1403
+ candidate: {
1404
+ customerId: "1001",
1405
+ name: "候选人A",
1406
+ sourceJob: "算法工程师",
1407
+ domIndex: 0,
1408
+ },
1409
+ totalVisibleCandidates: 1,
1410
+ readyState: {
1411
+ hasOnlineResume: true,
1412
+ hasAskResume: true,
1413
+ hasAttachmentResume: false,
1414
+ },
1415
+ };
1416
+ },
1417
+ async getLoadedCustomers() {
1418
+ calls.push("getLoadedCustomers:1");
1419
+ return [];
1420
+ },
1421
+ async closeResumeModalDomOnce() {
1422
+ return {
1423
+ closed: true,
1424
+ method: "already-closed",
1425
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1426
+ };
1427
+ },
1428
+ };
1429
+ const stateStore = {
1430
+ async load() {},
1431
+ hasAny() {
1432
+ return false;
1433
+ },
1434
+ async record() {},
1435
+ };
1436
+ const app = new BossChatApp({
1437
+ page,
1438
+ llmClient: {},
1439
+ interaction: {
1440
+ async sleepRange() {},
1441
+ async maybeRest() {},
1442
+ },
1443
+ resumeCaptureService: {},
1444
+ stateStore,
1445
+ reportStore: {
1446
+ async write() {
1447
+ return "report.json";
1448
+ },
1449
+ },
1450
+ logger: { log() {} },
1451
+ dryRun: true,
1452
+ artifactRootDir: os.tmpdir(),
1453
+ resumeOpenCooldownMs: 0,
1454
+ });
1455
+ app.waitForCandidateList = async ({ reason } = {}) => {
1456
+ calls.push(`waitForCandidateList:${reason || "unknown"}`);
1457
+ return {
1458
+ ready: true,
1459
+ waitedMs: 0,
1460
+ attempts: 1,
1461
+ listItemCount: 1,
1462
+ lastError: "",
1463
+ };
1464
+ };
1465
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1466
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1467
+ return {
1468
+ name: "候选人A",
1469
+ passed: false,
1470
+ requested: false,
1471
+ reason: "skip",
1472
+ error: "",
1473
+ artifacts: {},
1474
+ };
1475
+ };
1476
+
1477
+ const summary = await app.run({
1478
+ screeningCriteria: "有 AI 项目经验",
1479
+ targetCount: 1,
1480
+ startFrom: "unread",
1481
+ jobSelection: { label: "算法工程师", value: "job-1" },
1482
+ chrome: { port: 9222 },
1483
+ llm: { model: "gpt-test" },
1484
+ });
1485
+
1486
+ assert.deepEqual(calls.slice(0, 4), [
1487
+ "ensureReady",
1488
+ "activatePrimaryChatLabel:全部",
1489
+ "selectJob:算法工程师",
1490
+ "activateUnreadFilter",
1491
+ ]);
1492
+ assert.equal(calls.includes("primeConversationByFirstCandidate:1"), true);
1493
+ assert.equal(calls.includes("processCustomer:skip"), true);
1494
+ assert.equal(summary.inspected, 1);
1495
+ assert.equal(summary.skipped, 1);
1496
+ }
1497
+
1498
+ async function testBossChatAppShouldCloseCandidateDetailDuringRunCleanup() {
1499
+ const calls = [];
1500
+ const page = {
1501
+ async ensureReady() {
1502
+ calls.push("ensureReady");
1503
+ return { hasListContainer: true, listItemCount: 1 };
1504
+ },
1505
+ async activatePrimaryChatLabel(label) {
1506
+ calls.push(`activatePrimaryChatLabel:${label}`);
1507
+ return { changed: false, verified: true, activeLabel: label };
1508
+ },
1509
+ async selectJob(jobSelection) {
1510
+ calls.push(`selectJob:${jobSelection.label}`);
1511
+ return jobSelection;
1512
+ },
1513
+ async activateUnreadFilter() {
1514
+ calls.push("activateUnreadFilter");
1515
+ return { changed: false, verified: true, activeLabel: "未读" };
1516
+ },
1517
+ async primeConversationByFirstCandidate() {
1518
+ calls.push("primeConversationByFirstCandidate:1");
1519
+ return {
1520
+ candidate: {
1521
+ customerId: "1008",
1522
+ name: "候选人清理",
1523
+ sourceJob: "算法工程师",
1524
+ domIndex: 0
1525
+ },
1526
+ totalVisibleCandidates: 1,
1527
+ readyState: {
1528
+ hasOnlineResume: true,
1529
+ hasAskResume: true,
1530
+ hasAttachmentResume: false
1531
+ }
1532
+ };
1533
+ },
1534
+ async getLoadedCustomers() {
1535
+ calls.push("getLoadedCustomers:1");
1536
+ return [];
1537
+ },
1538
+ async closeResumeModalDomOnce() {
1539
+ calls.push("closeResumeModalDomOnce");
1540
+ return {
1541
+ closed: true,
1542
+ method: "already-closed",
1543
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" }
1544
+ };
1545
+ },
1546
+ async closeCandidateDetailDomOnce() {
1547
+ calls.push("closeCandidateDetailDomOnce");
1548
+ return {
1549
+ closed: true,
1550
+ method: "dom-close-once:.close-btn",
1551
+ finalState: { panelCount: 0, closeCount: 0, topPanelClass: "" }
1552
+ };
1553
+ }
1554
+ };
1555
+ const stateStore = {
1556
+ async load() {},
1557
+ hasAny() {
1558
+ return false;
1559
+ },
1560
+ async record() {}
1561
+ };
1562
+ const app = new BossChatApp({
1563
+ page,
1564
+ llmClient: {},
1565
+ interaction: {
1566
+ async sleepRange() {},
1567
+ async maybeRest() {}
1568
+ },
1569
+ resumeCaptureService: {},
1570
+ stateStore,
1571
+ reportStore: {
1572
+ async write() {
1573
+ return "report.json";
1574
+ }
1575
+ },
1576
+ logger: { log() {} },
1577
+ dryRun: true,
1578
+ artifactRootDir: os.tmpdir(),
1579
+ resumeOpenCooldownMs: 0
1580
+ });
1581
+ app.waitForCandidateList = async ({ reason } = {}) => {
1582
+ calls.push(`waitForCandidateList:${reason || "unknown"}`);
1583
+ return {
1584
+ ready: true,
1585
+ waitedMs: 0,
1586
+ attempts: 1,
1587
+ listItemCount: 1,
1588
+ lastError: ""
1589
+ };
1590
+ };
1591
+ app.processCustomer = async () => ({
1592
+ name: "候选人清理",
1593
+ passed: false,
1594
+ requested: false,
1595
+ reason: "skip",
1596
+ error: "",
1597
+ artifacts: {}
1598
+ });
1599
+
1600
+ const summary = await app.run({
1601
+ screeningCriteria: "有 AI 项目经验",
1602
+ targetCount: 1,
1603
+ startFrom: "unread",
1604
+ jobSelection: { label: "算法工程师", value: "job-1" },
1605
+ chrome: { port: 9222 },
1606
+ llm: { model: "gpt-test" }
1607
+ });
1608
+
1609
+ assert.equal(summary.inspected, 1);
1610
+ assert.equal(calls.includes("closeCandidateDetailDomOnce"), true);
1611
+ assert.equal(calls.lastIndexOf("closeCandidateDetailDomOnce") > calls.indexOf("getLoadedCustomers:1"), true);
1612
+ }
1613
+
1614
+ async function testBossChatAppShouldRestoreListContextAfterRecovery() {
1615
+ const calls = [];
1616
+ let primeCount = 0;
1617
+ let loadedCount = 0;
1618
+ const page = {
1619
+ async ensureReady() {
1620
+ return { hasListContainer: true, listItemCount: 1 };
1621
+ },
1622
+ async activatePrimaryChatLabel(label) {
1623
+ calls.push(`activatePrimaryChatLabel:${label}`);
1624
+ return { changed: false, verified: true, activeLabel: label };
1625
+ },
1626
+ async selectJob(jobSelection) {
1627
+ calls.push(`selectJob:${jobSelection.label}`);
1628
+ return jobSelection;
1629
+ },
1630
+ async activateUnreadFilter() {
1631
+ calls.push("activateUnreadFilter");
1632
+ return { changed: false, verified: true, activeLabel: "未读" };
1633
+ },
1634
+ async primeConversationByFirstCandidate() {
1635
+ primeCount += 1;
1636
+ calls.push(`primeConversationByFirstCandidate:${primeCount}`);
1637
+ if (primeCount === 1) {
1638
+ throw new Error("NO_FIRST_CANDIDATE");
1639
+ }
1640
+ return {
1641
+ candidate: {
1642
+ customerId: "1002",
1643
+ name: "候选人B",
1644
+ sourceJob: "算法工程师",
1645
+ domIndex: 0,
1646
+ },
1647
+ totalVisibleCandidates: 1,
1648
+ readyState: {
1649
+ hasOnlineResume: true,
1650
+ hasAskResume: true,
1651
+ hasAttachmentResume: false,
1652
+ },
1653
+ };
1654
+ },
1655
+ async getLoadedCustomers() {
1656
+ loadedCount += 1;
1657
+ calls.push(`getLoadedCustomers:${loadedCount}`);
1658
+ if (loadedCount === 1) {
1659
+ throw new Error("CHAT_CARD_LIST_NOT_FOUND");
1660
+ }
1661
+ return [];
1662
+ },
1663
+ async recoverToChatIndex() {
1664
+ calls.push("recoverToChatIndex");
1665
+ return { changed: true, href: "https://www.zhipin.com/web/chat/index" };
1666
+ },
1667
+ async closeResumeModalDomOnce() {
1668
+ return {
1669
+ closed: true,
1670
+ method: "already-closed",
1671
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1672
+ };
1673
+ },
1674
+ };
1675
+ const stateStore = {
1676
+ async load() {},
1677
+ hasAny() {
1678
+ return false;
1679
+ },
1680
+ async record() {},
1681
+ };
1682
+ const app = new BossChatApp({
1683
+ page,
1684
+ llmClient: {},
1685
+ interaction: {
1686
+ async sleepRange() {},
1687
+ async maybeRest() {},
1688
+ },
1689
+ resumeCaptureService: {},
1690
+ stateStore,
1691
+ reportStore: {
1692
+ async write() {
1693
+ return "report.json";
1694
+ },
1695
+ },
1696
+ logger: { log() {} },
1697
+ dryRun: true,
1698
+ artifactRootDir: os.tmpdir(),
1699
+ resumeOpenCooldownMs: 0,
1700
+ });
1701
+ app.waitForCandidateList = async ({ reason } = {}) => {
1702
+ calls.push(`waitForCandidateList:${reason || "unknown"}`);
1703
+ return {
1704
+ ready:
1705
+ reason === "initial-context-restore" ||
1706
+ reason === "post-recovery-context-restore",
1707
+ waitedMs: 0,
1708
+ attempts: 1,
1709
+ listItemCount:
1710
+ reason === "initial-context-restore" ||
1711
+ reason === "post-recovery-context-restore"
1712
+ ? 1
1713
+ : 0,
1714
+ lastError: "",
1715
+ };
1716
+ };
1717
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1718
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1719
+ return {
1720
+ name: "候选人B",
1721
+ passed: false,
1722
+ requested: false,
1723
+ reason: "skip",
1724
+ error: "",
1725
+ artifacts: {},
1726
+ };
1727
+ };
1728
+
1729
+ const summary = await app.run({
1730
+ screeningCriteria: "有 AI 项目经验",
1731
+ targetCount: 1,
1732
+ startFrom: "unread",
1733
+ jobSelection: { label: "算法工程师", value: "job-1" },
1734
+ chrome: { port: 9222 },
1735
+ llm: { model: "gpt-test" },
1736
+ });
1737
+
1738
+ assert.equal(calls.filter((item) => item === "activatePrimaryChatLabel:全部").length, 2);
1739
+ const recoverIndex = calls.indexOf("recoverToChatIndex");
1740
+ assert.equal(recoverIndex >= 0, true);
1741
+ assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
1742
+ assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
1743
+ assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
1744
+ assert.equal(calls[recoverIndex + 4], "waitForCandidateList:post-recovery-context-restore");
1745
+ assert.equal(calls[recoverIndex + 5], "primeConversationByFirstCandidate:2");
1746
+ assert.equal(calls.includes("processCustomer:skip"), true);
1747
+ assert.equal(summary.inspected, 1);
1748
+ assert.equal(summary.skipped, 1);
1749
+ }
1750
+
1751
+ async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
1752
+ const calls = [];
1753
+ let pageStateCall = 0;
1754
+ const page = {
1755
+ async ensureReady() {
1756
+ calls.push("ensureReady");
1757
+ return { hasListContainer: false, listItemCount: 0 };
1758
+ },
1759
+ async activatePrimaryChatLabel(label) {
1760
+ calls.push(`activatePrimaryChatLabel:${label}`);
1761
+ return { changed: false, verified: true, activeLabel: label };
1762
+ },
1763
+ async selectJob(jobSelection) {
1764
+ calls.push(`selectJob:${jobSelection.label}`);
1765
+ return jobSelection;
1766
+ },
1767
+ async activateUnreadFilter() {
1768
+ calls.push("activateUnreadFilter");
1769
+ return { changed: true, verified: true, activeLabel: "未读" };
1770
+ },
1771
+ async getPageState() {
1772
+ pageStateCall += 1;
1773
+ calls.push(`getPageState:${pageStateCall}`);
1774
+ return {
1775
+ href: "https://www.zhipin.com/web/chat/index",
1776
+ readyState: "complete",
1777
+ hasListContainer: pageStateCall >= 3,
1778
+ listItemCount: pageStateCall >= 3 ? 2 : 0,
1779
+ };
1780
+ },
1781
+ async primeConversationByFirstCandidate() {
1782
+ calls.push("primeConversationByFirstCandidate:1");
1783
+ return {
1784
+ candidate: {
1785
+ customerId: "1003",
1786
+ name: "候选人C",
1787
+ sourceJob: "算法工程师",
1788
+ domIndex: 0,
1789
+ },
1790
+ totalVisibleCandidates: 2,
1791
+ readyState: {
1792
+ hasOnlineResume: true,
1793
+ hasAskResume: true,
1794
+ hasAttachmentResume: false,
1795
+ },
1796
+ };
1797
+ },
1798
+ async getLoadedCustomers() {
1799
+ calls.push("getLoadedCustomers:1");
1800
+ return [];
1801
+ },
1802
+ async closeResumeModalDomOnce() {
1803
+ return {
1804
+ closed: true,
1805
+ method: "already-closed",
1806
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1807
+ };
1808
+ },
1809
+ };
1810
+ const stateStore = {
1811
+ async load() {},
1812
+ hasAny() {
1813
+ return false;
1814
+ },
1815
+ async record() {},
1816
+ };
1817
+ const app = new BossChatApp({
1818
+ page,
1819
+ llmClient: {},
1820
+ interaction: {
1821
+ async sleepRange() {},
1822
+ async maybeRest() {},
1823
+ },
1824
+ resumeCaptureService: {},
1825
+ stateStore,
1826
+ reportStore: {
1827
+ async write() {
1828
+ return "report.json";
1829
+ },
1830
+ },
1831
+ logger: { log() {} },
1832
+ dryRun: true,
1833
+ artifactRootDir: os.tmpdir(),
1834
+ resumeOpenCooldownMs: 0,
1835
+ });
1836
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1837
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1838
+ return {
1839
+ name: "候选人C",
1840
+ passed: false,
1841
+ requested: false,
1842
+ reason: "skip",
1843
+ error: "",
1844
+ artifacts: {},
1845
+ };
1846
+ };
1847
+
1848
+ const summary = await app.run({
1849
+ screeningCriteria: "有 AI 项目经验",
1850
+ targetCount: 1,
1851
+ startFrom: "unread",
1852
+ jobSelection: { label: "算法工程师", value: "job-1" },
1853
+ chrome: { port: 9222 },
1854
+ llm: { model: "gpt-test" },
1855
+ });
1856
+
1857
+ const primeIndex = calls.indexOf("primeConversationByFirstCandidate:1");
1858
+ const thirdStateIndex = calls.indexOf("getPageState:3");
1859
+ assert.equal(thirdStateIndex >= 0, true);
1860
+ assert.equal(primeIndex > thirdStateIndex, true);
1861
+ assert.equal(summary.inspected, 1);
1862
+ assert.equal(summary.skipped, 1);
1863
+ }
1864
+
1708
1865
  function createProcessCustomerHarness({
1709
1866
  llmEvaluate,
1710
1867
  captureResume,
1711
1868
  tracker,
1712
1869
  pageOverrides = {},
1870
+ dryRun = true,
1713
1871
  } = {}) {
1714
1872
  const recorded = [];
1715
1873
  const page = {
1716
1874
  async closeResumeModalDomOnce() {
1717
1875
  recorded.push("closeResumeModalDomOnce");
1876
+ return {
1877
+ closed: true,
1878
+ method: "dom",
1879
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1880
+ };
1881
+ },
1882
+ async closeResumeModal() {
1883
+ recorded.push("closeResumeModal");
1884
+ return {
1885
+ closed: true,
1886
+ method: "closeResumeModal",
1887
+ finalState: { open: false, scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1888
+ };
1889
+ },
1890
+ async closeCandidateDetailDomOnce() {
1891
+ recorded.push("closeCandidateDetailDomOnce");
1718
1892
  return {
1719
1893
  closed: true,
1720
- method: "dom",
1721
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1894
+ method: "already-closed",
1895
+ finalState: {
1896
+ open: false,
1897
+ panelCount: 0,
1898
+ closeCount: 0,
1899
+ topPanelClass: "",
1900
+ overlayClass: "",
1901
+ contentClass: "",
1902
+ },
1903
+ };
1904
+ },
1905
+ async closeCandidateDetail() {
1906
+ recorded.push("closeCandidateDetail");
1907
+ return {
1908
+ closed: true,
1909
+ method: "closeCandidateDetail",
1910
+ finalState: {
1911
+ open: false,
1912
+ panelCount: 0,
1913
+ closeCount: 0,
1914
+ topPanelClass: "",
1915
+ overlayClass: "",
1916
+ contentClass: "",
1917
+ },
1722
1918
  };
1723
1919
  },
1724
- async waitForConversationReady() {
1725
- recorded.push("waitForConversationReady");
1920
+ async waitForConversationReady(options = {}) {
1921
+ recorded.push(options.requirePanelsClosed ? "waitForConversationReady:strict" : "waitForConversationReady");
1726
1922
  return {
1727
1923
  hasOnlineResume: true,
1728
1924
  hasAskResume: true,
1729
1925
  hasAttachmentResume: false,
1730
1926
  attachmentResumeEnabled: false,
1927
+ resumeModalOpen: false,
1928
+ candidateDetailOpen: false,
1929
+ panelsClosed: true,
1930
+ editorVisible: true,
1931
+ activeSubmit: true,
1932
+ hasAnySubmit: true,
1933
+ messageInputReady: true,
1731
1934
  };
1732
1935
  },
1733
- async openOnlineResume() {
1734
- recorded.push("openOnlineResume");
1735
- return { clicked: true, detectedOpen: true, by: "dom" };
1736
- },
1737
- async getResumeRateLimitWarning() {
1738
- return { hit: false, text: "" };
1739
- },
1936
+ async openOnlineResume() {
1937
+ recorded.push("openOnlineResume");
1938
+ return { clicked: true, detectedOpen: true, by: "dom" };
1939
+ },
1940
+ async getResumeRateLimitWarning() {
1941
+ return { hit: false, text: "" };
1942
+ },
1740
1943
  async getResumeModalState() {
1741
1944
  return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
1742
1945
  },
1946
+ async getCandidateDetailState() {
1947
+ return {
1948
+ open: false,
1949
+ panelCount: 0,
1950
+ closeCount: 0,
1951
+ topPanelClass: "",
1952
+ overlayClass: "",
1953
+ contentClass: "",
1954
+ panelRect: null,
1955
+ overlayRect: null,
1956
+ contentRect: null,
1957
+ closeRect: null,
1958
+ };
1959
+ },
1743
1960
  async waitForCandidateActivated() {
1744
1961
  recorded.push("waitForCandidateActivated");
1745
1962
  return { matched: true };
@@ -1748,262 +1965,564 @@ function createProcessCustomerHarness({
1748
1965
  recorded.push("activateCandidate");
1749
1966
  return { ok: true };
1750
1967
  },
1751
- ...pageOverrides,
1752
- };
1753
- const llmCalls = [];
1754
- const llmClient = {
1755
- async evaluateResume(payload) {
1756
- llmCalls.push(payload);
1757
- return llmEvaluate(payload);
1968
+ async setEditorMessage(message) {
1969
+ recorded.push("setEditorMessage");
1970
+ return { value: message, activeSubmit: true };
1758
1971
  },
1759
- };
1760
- const resumeCaptureService = {
1761
- async captureResume(payload) {
1762
- recorded.push("captureResume");
1763
- return captureResume(payload);
1972
+ async sendMessage() {
1973
+ recorded.push("sendMessage");
1974
+ return { sent: true, method: "dom-submit", cleared: true, editorAfter: "" };
1764
1975
  },
1765
- };
1766
- const stateStore = {
1767
- async record(_key, result) {
1768
- recorded.push(`record:${result.decision}`);
1976
+ async clickAskResume() {
1977
+ recorded.push("clickAskResume");
1978
+ return { ok: true, alreadyRequested: false };
1769
1979
  },
1770
- };
1771
- const app = new BossChatApp({
1772
- page,
1773
- llmClient,
1774
- interaction: {
1775
- async sleepRange() {},
1776
- async clickRect() {},
1980
+ async clickConfirmRequestResume() {
1981
+ recorded.push("clickConfirmRequestResume");
1982
+ return {
1983
+ ok: true,
1984
+ confirmed: true,
1985
+ requestedVerified: true,
1986
+ assumedRequested: false,
1987
+ uiState: { hasDisabledOperateAsk: true },
1988
+ };
1989
+ },
1990
+ async getResumeRequestMessageState() {
1991
+ recorded.push("getResumeRequestMessageState");
1992
+ return { ok: true, count: 0, lastText: "", recent: [] };
1993
+ },
1994
+ async waitForResumeRequestMessage() {
1995
+ recorded.push("waitForResumeRequestMessage");
1996
+ return {
1997
+ observed: true,
1998
+ state: {
1999
+ ok: true,
2000
+ count: 1,
2001
+ lastText: "简历请求已发送",
2002
+ recent: ["简历请求已发送"],
2003
+ },
2004
+ };
1777
2005
  },
1778
- resumeCaptureService,
2006
+ ...pageOverrides,
2007
+ };
2008
+ const llmCalls = [];
2009
+ const llmClient = {
2010
+ async evaluateResume(payload) {
2011
+ llmCalls.push(payload);
2012
+ return llmEvaluate(payload);
2013
+ },
2014
+ };
2015
+ const resumeCaptureService = {
2016
+ async captureResume(payload) {
2017
+ recorded.push("captureResume");
2018
+ return captureResume(payload);
2019
+ },
2020
+ };
2021
+ const stateStore = {
2022
+ async record(_key, result) {
2023
+ recorded.push(`record:${result.decision}`);
2024
+ },
2025
+ };
2026
+ const app = new BossChatApp({
2027
+ page,
2028
+ llmClient,
2029
+ interaction: {
2030
+ async sleepRange() {},
2031
+ async clickRect() {},
2032
+ },
2033
+ resumeCaptureService,
1779
2034
  resumeNetworkTracker: tracker || null,
1780
2035
  stateStore,
1781
2036
  reportStore: { async write() { return ""; } },
1782
- dryRun: true,
2037
+ dryRun,
1783
2038
  artifactRootDir: os.tmpdir(),
1784
2039
  resumeOpenCooldownMs: 0,
1785
2040
  logger: { log() {} },
1786
2041
  });
1787
- app.waitResumeOpenCooldown = async () => {};
1788
- return { app, llmCalls, recorded };
1789
- }
1790
-
1791
- async function testBossChatResumeTrackerShouldRetryInitialNetworkWait() {
1792
- const tracker = new ResumeNetworkTracker({
1793
- chromeClient: { Network: null },
1794
- logger: { log() {} },
1795
- });
1796
- const waits = [];
1797
- let callCount = 0;
1798
- tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1799
- waits.push(timeoutMs);
1800
- callCount += 1;
1801
- if (callCount === 2) {
1802
- return {
1803
- candidateInfo: { resumeText: "network resume" },
1804
- source: "geek_id_map",
1805
- waitedMs: 80,
1806
- };
1807
- }
1808
- return null;
1809
- };
1810
- const result = await tracker.waitForResumeNetworkByMode({ customerId: "1001" });
1811
- assert.deepEqual(waits, [NETWORK_RESUME_WAIT_MS, NETWORK_RESUME_RETRY_WAIT_MS]);
1812
- assert.equal(result.acquisitionReason, "network_retry_hit");
1813
- }
1814
-
1815
- async function testBossChatResumeTrackerShouldUseImageModeGraceWindow() {
1816
- const tracker = new ResumeNetworkTracker({
1817
- chromeClient: { Network: null },
1818
- logger: { log() {} },
1819
- });
1820
- tracker.setResumeAcquisitionMode("image", "previous_image_fallback");
1821
- const waits = [];
1822
- tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1823
- waits.push(timeoutMs);
1824
- return null;
1825
- };
1826
- const result = await tracker.waitForResumeNetworkByMode({ customerId: "1002" });
1827
- assert.deepEqual(waits, [NETWORK_RESUME_IMAGE_MODE_GRACE_MS]);
1828
- assert.equal(result.initialWaitMs >= 0, true);
1829
- assert.equal(result.retryWaitMs, 0);
2042
+ app.waitResumeOpenCooldown = async () => {};
2043
+ return { app, llmCalls, recorded };
2044
+ }
2045
+
2046
+ async function testBossChatResumeTrackerShouldRetryInitialNetworkWait() {
2047
+ const tracker = new ResumeNetworkTracker({
2048
+ chromeClient: { Network: null },
2049
+ logger: { log() {} },
2050
+ });
2051
+ const waits = [];
2052
+ let callCount = 0;
2053
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
2054
+ waits.push(timeoutMs);
2055
+ callCount += 1;
2056
+ if (callCount === 2) {
2057
+ return {
2058
+ candidateInfo: { resumeText: "network resume" },
2059
+ source: "geek_id_map",
2060
+ waitedMs: 80,
2061
+ };
2062
+ }
2063
+ return null;
2064
+ };
2065
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1001" });
2066
+ assert.deepEqual(waits, [NETWORK_RESUME_WAIT_MS, NETWORK_RESUME_RETRY_WAIT_MS]);
2067
+ assert.equal(result.acquisitionReason, "network_retry_hit");
2068
+ }
2069
+
2070
+ async function testBossChatResumeTrackerShouldUseImageModeGraceWindow() {
2071
+ const tracker = new ResumeNetworkTracker({
2072
+ chromeClient: { Network: null },
2073
+ logger: { log() {} },
2074
+ });
2075
+ tracker.setResumeAcquisitionMode("image", "previous_image_fallback");
2076
+ const waits = [];
2077
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
2078
+ waits.push(timeoutMs);
2079
+ return null;
2080
+ };
2081
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1002" });
2082
+ assert.deepEqual(waits, [NETWORK_RESUME_IMAGE_MODE_GRACE_MS]);
2083
+ assert.equal(result.initialWaitMs >= 0, true);
2084
+ assert.equal(result.retryWaitMs, 0);
2085
+ }
2086
+
2087
+ async function testBossChatAppShouldUseNetworkBeforeImageFallback() {
2088
+ const tracker = {
2089
+ resumeNetworkDiagnostics: [],
2090
+ getResumeAcquisitionState() {
2091
+ return { mode: "network", reason: "initial_network_hit" };
2092
+ },
2093
+ async waitForResumeNetworkByMode() {
2094
+ return {
2095
+ candidateInfo: {
2096
+ name: "候选人A",
2097
+ school: "清华大学",
2098
+ major: "计算机",
2099
+ company: "OpenAI",
2100
+ position: "工程师",
2101
+ resumeText: "清华大学 计算机 OpenAI",
2102
+ evidenceCorpus: "清华大学 计算机 OpenAI",
2103
+ },
2104
+ acquisitionReason: "initial_network_hit",
2105
+ initialWaitMs: 12,
2106
+ retryWaitMs: 0,
2107
+ };
2108
+ },
2109
+ async waitForLateNetworkResumeCandidateInfo() {
2110
+ throw new Error("late network retry should not run");
2111
+ },
2112
+ };
2113
+ const { app, llmCalls, recorded } = createProcessCustomerHarness({
2114
+ tracker,
2115
+ llmEvaluate: async () => ({
2116
+ passed: true,
2117
+ rawOutputText: '{"passed":true}',
2118
+ evaluationMode: "text",
2119
+ chunkIndex: 1,
2120
+ chunkTotal: 1,
2121
+ }),
2122
+ captureResume: async () => {
2123
+ throw new Error("image capture should not run");
2124
+ },
2125
+ });
2126
+
2127
+ const result = await app.processCustomer(
2128
+ {
2129
+ customerKey: "candidate-network",
2130
+ name: "候选人A",
2131
+ sourceJob: "算法工程师",
2132
+ domIndex: 0,
2133
+ customerId: "1001",
2134
+ textSnippet: "",
2135
+ },
2136
+ { screeningCriteria: "有 AI 项目经验" },
2137
+ "run-network",
2138
+ { skipCardClick: true },
2139
+ );
2140
+
2141
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
2142
+ assert.equal(result.artifacts.resumeAcquisitionReason, "initial_network_hit");
2143
+ assert.equal(llmCalls.length, 1);
2144
+ assert.equal(llmCalls[0].candidate.resumeText.includes("清华大学"), true);
2145
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), false);
2146
+ assert.equal(recorded.includes("captureResume"), false);
2147
+ }
2148
+
2149
+ async function testBossChatAppShouldFallbackToImageAfterNetworkMiss() {
2150
+ const tracker = {
2151
+ resumeNetworkDiagnostics: [],
2152
+ setResumeAcquisitionMode(mode, reason) {
2153
+ this.state = { mode, reason };
2154
+ },
2155
+ getResumeAcquisitionState() {
2156
+ return this.state || { mode: "image", reason: "image_capture_success" };
2157
+ },
2158
+ async waitForResumeNetworkByMode() {
2159
+ return {
2160
+ candidateInfo: null,
2161
+ acquisitionReason: "",
2162
+ initialWaitMs: 10,
2163
+ retryWaitMs: 20,
2164
+ };
2165
+ },
2166
+ async waitForLateNetworkResumeCandidateInfo() {
2167
+ return {
2168
+ candidateInfo: null,
2169
+ acquisitionReason: "",
2170
+ lateRetryMs: 0,
2171
+ };
2172
+ },
2173
+ };
2174
+ const { app, llmCalls } = createProcessCustomerHarness({
2175
+ tracker,
2176
+ llmEvaluate: async () => ({
2177
+ passed: false,
2178
+ rawOutputText: '{"passed":false}',
2179
+ evaluationMode: "image-multi-chunk",
2180
+ imageCount: 2,
2181
+ chunkIndex: 1,
2182
+ chunkTotal: 1,
2183
+ }),
2184
+ captureResume: async ({ artifactDir }) => ({
2185
+ metadataFile: path.join(artifactDir, "chunks.json"),
2186
+ chunkDir: path.join(artifactDir, "chunks"),
2187
+ chunkCount: 2,
2188
+ modelImagePaths: [
2189
+ path.join(artifactDir, "chunks", "chunk_000.png"),
2190
+ path.join(artifactDir, "chunks", "chunk_001.png"),
2191
+ ],
2192
+ stitchedImage: "",
2193
+ quality: { likelyBlank: false },
2194
+ }),
2195
+ });
2196
+
2197
+ const result = await app.processCustomer(
2198
+ {
2199
+ customerKey: "candidate-image",
2200
+ name: "候选人B",
2201
+ sourceJob: "算法工程师",
2202
+ domIndex: 0,
2203
+ customerId: "1002",
2204
+ textSnippet: "",
2205
+ },
2206
+ { screeningCriteria: "有 AI 项目经验" },
2207
+ "run-image",
2208
+ { skipCardClick: true },
2209
+ );
2210
+
2211
+ assert.equal(result.artifacts.resumeAcquisitionMode, "image_fallback");
2212
+ assert.equal(result.artifacts.resumeAcquisitionReason, "image_capture_success");
2213
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), true);
2214
+ assert.equal(llmCalls[0].imagePaths.length, 2);
2215
+ }
2216
+
2217
+ async function testBossChatAppShouldRetryLateNetworkBeforeDomFallback() {
2218
+ const tracker = {
2219
+ resumeNetworkDiagnostics: [],
2220
+ getResumeAcquisitionState() {
2221
+ return { mode: "network", reason: "late_network_hit" };
2222
+ },
2223
+ setResumeAcquisitionMode() {},
2224
+ async waitForResumeNetworkByMode() {
2225
+ return {
2226
+ candidateInfo: null,
2227
+ acquisitionReason: "",
2228
+ initialWaitMs: 10,
2229
+ retryWaitMs: 20,
2230
+ };
2231
+ },
2232
+ async waitForLateNetworkResumeCandidateInfo() {
2233
+ return {
2234
+ candidateInfo: {
2235
+ name: "候选人C",
2236
+ school: "上海交大",
2237
+ major: "软件工程",
2238
+ resumeText: "上海交大 软件工程",
2239
+ evidenceCorpus: "上海交大 软件工程",
2240
+ },
2241
+ acquisitionReason: "late_network_hit",
2242
+ lateRetryMs: 30,
2243
+ };
2244
+ },
2245
+ };
2246
+ let imageAttempt = 0;
2247
+ const { app, llmCalls } = createProcessCustomerHarness({
2248
+ tracker,
2249
+ llmEvaluate: async (payload) => {
2250
+ imageAttempt += 1;
2251
+ if (Array.isArray(payload.imagePaths) && payload.imagePaths.length > 0) {
2252
+ throw new Error("VISION_MODEL_FAILED");
2253
+ }
2254
+ return {
2255
+ passed: true,
2256
+ rawOutputText: '{"passed":true}',
2257
+ evaluationMode: "text",
2258
+ chunkIndex: 1,
2259
+ chunkTotal: 1,
2260
+ };
2261
+ },
2262
+ captureResume: async ({ artifactDir }) => ({
2263
+ metadataFile: path.join(artifactDir, "chunks.json"),
2264
+ chunkDir: path.join(artifactDir, "chunks"),
2265
+ chunkCount: 1,
2266
+ modelImagePaths: [path.join(artifactDir, "chunks", "chunk_000.png")],
2267
+ stitchedImage: "",
2268
+ quality: { likelyBlank: false },
2269
+ }),
2270
+ });
2271
+
2272
+ const result = await app.processCustomer(
2273
+ {
2274
+ customerKey: "candidate-late-network",
2275
+ name: "候选人C",
2276
+ sourceJob: "算法工程师",
2277
+ domIndex: 0,
2278
+ customerId: "1003",
2279
+ textSnippet: "",
2280
+ },
2281
+ { screeningCriteria: "有 AI 项目经验" },
2282
+ "run-late-network",
2283
+ { skipCardClick: true },
2284
+ );
2285
+
2286
+ assert.equal(imageAttempt >= 2, true);
2287
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
2288
+ assert.equal(result.artifacts.resumeAcquisitionReason, "late_network_hit");
2289
+ assert.equal(llmCalls[llmCalls.length - 1].candidate.resumeText.includes("上海交大"), true);
2290
+ }
2291
+
2292
+ async function testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail() {
2293
+ let domReadCount = 0;
2294
+ const tracker = {
2295
+ resumeNetworkDiagnostics: [],
2296
+ getResumeAcquisitionState() {
2297
+ return { mode: "image", reason: "image_capture_success" };
2298
+ },
2299
+ setResumeAcquisitionMode() {},
2300
+ async waitForResumeNetworkByMode() {
2301
+ return {
2302
+ candidateInfo: null,
2303
+ acquisitionReason: "",
2304
+ initialWaitMs: 10,
2305
+ retryWaitMs: 20,
2306
+ };
2307
+ },
2308
+ async waitForLateNetworkResumeCandidateInfo() {
2309
+ return {
2310
+ candidateInfo: null,
2311
+ acquisitionReason: "",
2312
+ lateRetryMs: 15,
2313
+ };
2314
+ },
2315
+ async waitForNetworkResumeCandidateInfo() {
2316
+ return null;
2317
+ },
2318
+ };
2319
+ const { app, recorded } = createProcessCustomerHarness({
2320
+ tracker,
2321
+ llmEvaluate: async (payload) => ({
2322
+ passed: false,
2323
+ rawOutputText: '{"passed":false}',
2324
+ evaluationMode: "text",
2325
+ chunkIndex: 1,
2326
+ chunkTotal: 1,
2327
+ imageCount: 0,
2328
+ }),
2329
+ captureResume: async () => {
2330
+ throw new Error("IMAGE_CAPTURE_FAILED");
2331
+ },
2332
+ pageOverrides: {
2333
+ async getResumeProfileFromDom() {
2334
+ domReadCount += 1;
2335
+ if (domReadCount === 1) {
2336
+ return {
2337
+ ok: true,
2338
+ name: "李同学",
2339
+ primarySchool: "北京大学",
2340
+ schools: ["北京大学"],
2341
+ major: "数学",
2342
+ majors: ["数学"],
2343
+ company: "",
2344
+ position: "",
2345
+ resumeText: "北京大学 数学",
2346
+ evidenceCorpus: "北京大学 数学",
2347
+ };
2348
+ }
2349
+ return {
2350
+ ok: true,
2351
+ name: "候选人D",
2352
+ primarySchool: "浙江大学",
2353
+ schools: ["浙江大学"],
2354
+ major: "计算机",
2355
+ majors: ["计算机"],
2356
+ company: "",
2357
+ position: "",
2358
+ resumeText: "浙江大学 计算机",
2359
+ evidenceCorpus: "浙江大学 计算机",
2360
+ };
2361
+ },
2362
+ },
2363
+ });
2364
+
2365
+ const result = await app.processCustomer(
2366
+ {
2367
+ customerKey: "candidate-dom",
2368
+ name: "候选人D",
2369
+ school: "浙江大学",
2370
+ major: "计算机",
2371
+ sourceJob: "算法工程师",
2372
+ domIndex: 0,
2373
+ customerId: "1004",
2374
+ textSnippet: "",
2375
+ },
2376
+ { screeningCriteria: "有 AI 项目经验" },
2377
+ "run-dom",
2378
+ { skipCardClick: true },
2379
+ );
2380
+
2381
+ assert.equal(result.artifacts.resumeAcquisitionMode, "dom_fallback");
2382
+ assert.equal(result.artifacts.resumeAcquisitionReason, "dom_retry_hit");
2383
+ assert.equal(domReadCount, 2);
2384
+ assert.equal(recorded.includes("activateCandidate"), true);
2385
+ assert.equal(recorded.includes("openOnlineResume"), true);
1830
2386
  }
1831
2387
 
1832
- async function testBossChatAppShouldUseNetworkBeforeImageFallback() {
1833
- const tracker = {
1834
- resumeNetworkDiagnostics: [],
1835
- getResumeAcquisitionState() {
1836
- return { mode: "network", reason: "initial_network_hit" };
1837
- },
1838
- async waitForResumeNetworkByMode() {
1839
- return {
1840
- candidateInfo: {
1841
- name: "候选人A",
1842
- school: "清华大学",
1843
- major: "计算机",
1844
- company: "OpenAI",
1845
- position: "工程师",
1846
- resumeText: "清华大学 计算机 OpenAI",
1847
- evidenceCorpus: "清华大学 计算机 OpenAI",
1848
- },
1849
- acquisitionReason: "initial_network_hit",
1850
- initialWaitMs: 12,
1851
- retryWaitMs: 0,
1852
- };
1853
- },
1854
- async waitForLateNetworkResumeCandidateInfo() {
1855
- throw new Error("late network retry should not run");
1856
- },
1857
- };
1858
- const { app, llmCalls, recorded } = createProcessCustomerHarness({
1859
- tracker,
2388
+ async function testBossChatAppShouldGateOutreachUntilPanelsClosed() {
2389
+ let resumeModalOpen = true;
2390
+ let detailOpen = true;
2391
+ const { app, recorded } = createProcessCustomerHarness({
2392
+ dryRun: false,
1860
2393
  llmEvaluate: async () => ({
1861
2394
  passed: true,
1862
2395
  rawOutputText: '{"passed":true}',
1863
- evaluationMode: "text",
1864
- chunkIndex: 1,
1865
- chunkTotal: 1,
1866
- }),
1867
- captureResume: async () => {
1868
- throw new Error("image capture should not run");
1869
- },
1870
- });
1871
-
1872
- const result = await app.processCustomer(
1873
- {
1874
- customerKey: "candidate-network",
1875
- name: "候选人A",
1876
- sourceJob: "算法工程师",
1877
- domIndex: 0,
1878
- customerId: "1001",
1879
- textSnippet: "",
1880
- },
1881
- { screeningCriteria: "有 AI 项目经验" },
1882
- "run-network",
1883
- { skipCardClick: true },
1884
- );
1885
-
1886
- assert.equal(result.artifacts.resumeAcquisitionMode, "network");
1887
- assert.equal(result.artifacts.resumeAcquisitionReason, "initial_network_hit");
1888
- assert.equal(llmCalls.length, 1);
1889
- assert.equal(llmCalls[0].candidate.resumeText.includes("清华大学"), true);
1890
- assert.equal(Array.isArray(llmCalls[0].imagePaths), false);
1891
- assert.equal(recorded.includes("captureResume"), false);
1892
- }
1893
-
1894
- async function testBossChatAppShouldFallbackToImageAfterNetworkMiss() {
1895
- const tracker = {
1896
- resumeNetworkDiagnostics: [],
1897
- setResumeAcquisitionMode(mode, reason) {
1898
- this.state = { mode, reason };
1899
- },
1900
- getResumeAcquisitionState() {
1901
- return this.state || { mode: "image", reason: "image_capture_success" };
1902
- },
1903
- async waitForResumeNetworkByMode() {
1904
- return {
1905
- candidateInfo: null,
1906
- acquisitionReason: "",
1907
- initialWaitMs: 10,
1908
- retryWaitMs: 20,
1909
- };
1910
- },
1911
- async waitForLateNetworkResumeCandidateInfo() {
1912
- return {
1913
- candidateInfo: null,
1914
- acquisitionReason: "",
1915
- lateRetryMs: 0,
1916
- };
1917
- },
1918
- };
1919
- const { app, llmCalls } = createProcessCustomerHarness({
1920
- tracker,
1921
- llmEvaluate: async () => ({
1922
- passed: false,
1923
- rawOutputText: '{"passed":false}',
1924
2396
  evaluationMode: "image-multi-chunk",
1925
- imageCount: 2,
2397
+ imageCount: 1,
1926
2398
  chunkIndex: 1,
1927
2399
  chunkTotal: 1,
1928
2400
  }),
1929
2401
  captureResume: async ({ artifactDir }) => ({
1930
2402
  metadataFile: path.join(artifactDir, "chunks.json"),
1931
2403
  chunkDir: path.join(artifactDir, "chunks"),
1932
- chunkCount: 2,
1933
- modelImagePaths: [
1934
- path.join(artifactDir, "chunks", "chunk_000.png"),
1935
- path.join(artifactDir, "chunks", "chunk_001.png"),
1936
- ],
2404
+ chunkCount: 1,
2405
+ modelImagePaths: [path.join(artifactDir, "chunks", "chunk_000.png")],
1937
2406
  stitchedImage: "",
1938
2407
  quality: { likelyBlank: false },
1939
2408
  }),
2409
+ pageOverrides: {
2410
+ async closeResumeModalDomOnce() {
2411
+ recorded.push("closeResumeModalDomOnce");
2412
+ resumeModalOpen = false;
2413
+ return {
2414
+ closed: true,
2415
+ method: "dom-close-once:.boss-popup__close",
2416
+ finalState: { open: false, scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
2417
+ };
2418
+ },
2419
+ async getResumeModalState() {
2420
+ return {
2421
+ open: resumeModalOpen,
2422
+ iframeCount: resumeModalOpen ? 1 : 0,
2423
+ scopeCount: resumeModalOpen ? 1 : 0,
2424
+ closeCount: resumeModalOpen ? 1 : 0,
2425
+ topScopeClass: resumeModalOpen ? "resume-modal" : "",
2426
+ };
2427
+ },
2428
+ async closeCandidateDetailDomOnce() {
2429
+ recorded.push("closeCandidateDetailDomOnce");
2430
+ detailOpen = false;
2431
+ return {
2432
+ closed: true,
2433
+ method: "dom-close-once:.close-btn",
2434
+ finalState: {
2435
+ open: false,
2436
+ panelCount: 0,
2437
+ closeCount: 0,
2438
+ topPanelClass: "",
2439
+ overlayClass: "",
2440
+ contentClass: "",
2441
+ },
2442
+ };
2443
+ },
2444
+ async getCandidateDetailState() {
2445
+ return {
2446
+ open: detailOpen,
2447
+ panelCount: detailOpen ? 1 : 0,
2448
+ closeCount: detailOpen ? 1 : 0,
2449
+ topPanelClass: detailOpen ? "new-resume-online-main-ui" : "",
2450
+ overlayClass: detailOpen ? "dialog-wrap active" : "",
2451
+ contentClass: detailOpen ? "new-resume-online-main-ui resume-recommend resume-common-wrap" : "",
2452
+ panelRect: detailOpen ? { left: 980, top: 0, width: 330, height: 760, right: 1310, bottom: 760 } : null,
2453
+ overlayRect: detailOpen ? { left: 0, top: 0, width: 1440, height: 773, right: 1440, bottom: 773 } : null,
2454
+ contentRect: detailOpen ? { left: 980, top: 0, width: 330, height: 760, right: 1310, bottom: 760 } : null,
2455
+ closeRect: detailOpen ? { left: 1274, top: 12, width: 30, height: 30, right: 1304, bottom: 42 } : null,
2456
+ };
2457
+ },
2458
+ async waitForConversationReady(options = {}) {
2459
+ recorded.push(options.requirePanelsClosed ? "waitForConversationReady:strict" : "waitForConversationReady");
2460
+ if (options.requirePanelsClosed && (resumeModalOpen || detailOpen)) {
2461
+ throw new Error("CONVERSATION_PANEL_NOT_READY_OR_BLOCKED");
2462
+ }
2463
+ return {
2464
+ hasOnlineResume: true,
2465
+ hasAskResume: true,
2466
+ hasAttachmentResume: false,
2467
+ attachmentResumeEnabled: false,
2468
+ resumeModalOpen,
2469
+ candidateDetailOpen: detailOpen,
2470
+ panelsClosed: !resumeModalOpen && !detailOpen,
2471
+ editorVisible: true,
2472
+ activeSubmit: true,
2473
+ hasAnySubmit: true,
2474
+ messageInputReady: true,
2475
+ };
2476
+ },
2477
+ },
1940
2478
  });
1941
2479
 
1942
2480
  const result = await app.processCustomer(
1943
2481
  {
1944
- customerKey: "candidate-image",
1945
- name: "候选人B",
2482
+ customerKey: "candidate-gated",
2483
+ name: "候选人G",
1946
2484
  sourceJob: "算法工程师",
1947
2485
  domIndex: 0,
1948
- customerId: "1002",
2486
+ customerId: "2001",
1949
2487
  textSnippet: "",
1950
2488
  },
1951
2489
  { screeningCriteria: "有 AI 项目经验" },
1952
- "run-image",
2490
+ "run-gated",
1953
2491
  { skipCardClick: true },
1954
2492
  );
1955
2493
 
1956
- assert.equal(result.artifacts.resumeAcquisitionMode, "image_fallback");
1957
- assert.equal(result.artifacts.resumeAcquisitionReason, "image_capture_success");
1958
- assert.equal(Array.isArray(llmCalls[0].imagePaths), true);
1959
- assert.equal(llmCalls[0].imagePaths.length, 2);
2494
+ const captureIndex = recorded.indexOf("captureResume");
2495
+ const closeResumeIndex = recorded.findIndex((item, index) => index > captureIndex && item === "closeResumeModalDomOnce");
2496
+ const closeDetailIndex = recorded.findIndex((item, index) => index > closeResumeIndex && item === "closeCandidateDetailDomOnce");
2497
+ const strictReadyIndex = recorded.findIndex((item, index) => index > closeDetailIndex && item === "waitForConversationReady:strict");
2498
+ const setEditorIndex = recorded.findIndex((item, index) => index > strictReadyIndex && item === "setEditorMessage");
2499
+ const sendMessageIndex = recorded.findIndex((item, index) => index > setEditorIndex && item === "sendMessage");
2500
+ const askResumeIndex = recorded.findIndex((item, index) => index > sendMessageIndex && item === "clickAskResume");
2501
+
2502
+ assert.equal(result.decision, "passed");
2503
+ assert.equal(result.requested, true);
2504
+ assert.equal(result.artifacts.preActionResumeClosed, true);
2505
+ assert.equal(result.artifacts.preActionDetailClosed, true);
2506
+ assert.equal(closeResumeIndex > captureIndex, true);
2507
+ assert.equal(closeDetailIndex > closeResumeIndex, true);
2508
+ assert.equal(strictReadyIndex > closeDetailIndex, true);
2509
+ assert.equal(setEditorIndex > strictReadyIndex, true);
2510
+ assert.equal(sendMessageIndex > setEditorIndex, true);
2511
+ assert.equal(askResumeIndex > sendMessageIndex, true);
1960
2512
  }
1961
2513
 
1962
- async function testBossChatAppShouldRetryLateNetworkBeforeDomFallback() {
1963
- const tracker = {
1964
- resumeNetworkDiagnostics: [],
1965
- getResumeAcquisitionState() {
1966
- return { mode: "network", reason: "late_network_hit" };
1967
- },
1968
- setResumeAcquisitionMode() {},
1969
- async waitForResumeNetworkByMode() {
1970
- return {
1971
- candidateInfo: null,
1972
- acquisitionReason: "",
1973
- initialWaitMs: 10,
1974
- retryWaitMs: 20,
1975
- };
1976
- },
1977
- async waitForLateNetworkResumeCandidateInfo() {
1978
- return {
1979
- candidateInfo: {
1980
- name: "候选人C",
1981
- school: "上海交大",
1982
- major: "软件工程",
1983
- resumeText: "上海交大 软件工程",
1984
- evidenceCorpus: "上海交大 软件工程",
1985
- },
1986
- acquisitionReason: "late_network_hit",
1987
- lateRetryMs: 30,
1988
- };
1989
- },
1990
- };
1991
- let imageAttempt = 0;
1992
- const { app, llmCalls } = createProcessCustomerHarness({
1993
- tracker,
1994
- llmEvaluate: async (payload) => {
1995
- imageAttempt += 1;
1996
- if (Array.isArray(payload.imagePaths) && payload.imagePaths.length > 0) {
1997
- throw new Error("VISION_MODEL_FAILED");
1998
- }
1999
- return {
2000
- passed: true,
2001
- rawOutputText: '{"passed":true}',
2002
- evaluationMode: "text",
2003
- chunkIndex: 1,
2004
- chunkTotal: 1,
2005
- };
2006
- },
2514
+ async function testBossChatAppShouldSkipOutreachWhenDetailStillOpenAfterRetry() {
2515
+ let resumeModalOpen = true;
2516
+ const { app, recorded } = createProcessCustomerHarness({
2517
+ dryRun: false,
2518
+ llmEvaluate: async () => ({
2519
+ passed: true,
2520
+ rawOutputText: '{"passed":true}',
2521
+ evaluationMode: "image-multi-chunk",
2522
+ imageCount: 1,
2523
+ chunkIndex: 1,
2524
+ chunkTotal: 1,
2525
+ }),
2007
2526
  captureResume: async ({ artifactDir }) => ({
2008
2527
  metadataFile: path.join(artifactDir, "chunks.json"),
2009
2528
  chunkDir: path.join(artifactDir, "chunks"),
@@ -2012,96 +2531,83 @@ async function testBossChatAppShouldRetryLateNetworkBeforeDomFallback() {
2012
2531
  stitchedImage: "",
2013
2532
  quality: { likelyBlank: false },
2014
2533
  }),
2015
- });
2016
-
2017
- const result = await app.processCustomer(
2018
- {
2019
- customerKey: "candidate-late-network",
2020
- name: "候选人C",
2021
- sourceJob: "算法工程师",
2022
- domIndex: 0,
2023
- customerId: "1003",
2024
- textSnippet: "",
2025
- },
2026
- { screeningCriteria: "有 AI 项目经验" },
2027
- "run-late-network",
2028
- { skipCardClick: true },
2029
- );
2030
-
2031
- assert.equal(imageAttempt >= 2, true);
2032
- assert.equal(result.artifacts.resumeAcquisitionMode, "network");
2033
- assert.equal(result.artifacts.resumeAcquisitionReason, "late_network_hit");
2034
- assert.equal(llmCalls[llmCalls.length - 1].candidate.resumeText.includes("上海交大"), true);
2035
- }
2036
-
2037
- async function testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail() {
2038
- let domReadCount = 0;
2039
- const tracker = {
2040
- resumeNetworkDiagnostics: [],
2041
- getResumeAcquisitionState() {
2042
- return { mode: "image", reason: "image_capture_success" };
2043
- },
2044
- setResumeAcquisitionMode() {},
2045
- async waitForResumeNetworkByMode() {
2046
- return {
2047
- candidateInfo: null,
2048
- acquisitionReason: "",
2049
- initialWaitMs: 10,
2050
- retryWaitMs: 20,
2051
- };
2052
- },
2053
- async waitForLateNetworkResumeCandidateInfo() {
2054
- return {
2055
- candidateInfo: null,
2056
- acquisitionReason: "",
2057
- lateRetryMs: 15,
2058
- };
2059
- },
2060
- async waitForNetworkResumeCandidateInfo() {
2061
- return null;
2062
- },
2063
- };
2064
- const { app, recorded } = createProcessCustomerHarness({
2065
- tracker,
2066
- llmEvaluate: async (payload) => ({
2067
- passed: false,
2068
- rawOutputText: '{"passed":false}',
2069
- evaluationMode: "text",
2070
- chunkIndex: 1,
2071
- chunkTotal: 1,
2072
- imageCount: 0,
2073
- }),
2074
- captureResume: async () => {
2075
- throw new Error("IMAGE_CAPTURE_FAILED");
2076
- },
2077
2534
  pageOverrides: {
2078
- async getResumeProfileFromDom() {
2079
- domReadCount += 1;
2080
- if (domReadCount === 1) {
2081
- return {
2082
- ok: true,
2083
- name: "李同学",
2084
- primarySchool: "北京大学",
2085
- schools: ["北京大学"],
2086
- major: "数学",
2087
- majors: ["数学"],
2088
- company: "",
2089
- position: "",
2090
- resumeText: "北京大学 数学",
2091
- evidenceCorpus: "北京大学 数学",
2092
- };
2093
- }
2535
+ async closeResumeModalDomOnce() {
2536
+ recorded.push("closeResumeModalDomOnce");
2537
+ resumeModalOpen = false;
2094
2538
  return {
2095
- ok: true,
2096
- name: "候选人D",
2097
- primarySchool: "浙江大学",
2098
- schools: ["浙江大学"],
2099
- major: "计算机",
2100
- majors: ["计算机"],
2101
- company: "",
2102
- position: "",
2103
- resumeText: "浙江大学 计算机",
2104
- evidenceCorpus: "浙江大学 计算机",
2539
+ closed: true,
2540
+ method: "dom-close-once:.boss-popup__close",
2541
+ finalState: { open: false, scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
2542
+ };
2543
+ },
2544
+ async getResumeModalState() {
2545
+ return {
2546
+ open: resumeModalOpen,
2547
+ iframeCount: resumeModalOpen ? 1 : 0,
2548
+ scopeCount: resumeModalOpen ? 1 : 0,
2549
+ closeCount: resumeModalOpen ? 1 : 0,
2550
+ topScopeClass: resumeModalOpen ? "resume-modal" : "",
2551
+ };
2552
+ },
2553
+ async closeCandidateDetailDomOnce() {
2554
+ recorded.push("closeCandidateDetailDomOnce");
2555
+ return {
2556
+ closed: false,
2557
+ method: "dom-close-miss:.close-btn",
2558
+ finalState: {
2559
+ open: true,
2560
+ panelCount: 1,
2561
+ closeCount: 1,
2562
+ topPanelClass: "new-resume-online-main-ui",
2563
+ overlayClass: "dialog-wrap active",
2564
+ contentClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
2565
+ },
2566
+ };
2567
+ },
2568
+ async closeCandidateDetail() {
2569
+ recorded.push("closeCandidateDetail");
2570
+ return {
2571
+ closed: false,
2572
+ method: "selector:.close-btn+escape+outside-click:left-gap",
2573
+ finalState: {
2574
+ open: true,
2575
+ panelCount: 1,
2576
+ closeCount: 1,
2577
+ topPanelClass: "new-resume-online-main-ui",
2578
+ overlayClass: "dialog-wrap active",
2579
+ contentClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
2580
+ },
2581
+ };
2582
+ },
2583
+ async getCandidateDetailState() {
2584
+ return {
2585
+ open: true,
2586
+ panelCount: 1,
2587
+ closeCount: 1,
2588
+ topPanelClass: "new-resume-online-main-ui",
2589
+ overlayClass: "dialog-wrap active",
2590
+ contentClass: "new-resume-online-main-ui resume-recommend resume-common-wrap",
2591
+ panelRect: { left: 980, top: 0, width: 330, height: 760, right: 1310, bottom: 760 },
2592
+ overlayRect: { left: 0, top: 0, width: 1440, height: 773, right: 1440, bottom: 773 },
2593
+ contentRect: { left: 980, top: 0, width: 330, height: 760, right: 1310, bottom: 760 },
2594
+ closeRect: { left: 1274, top: 12, width: 30, height: 30, right: 1304, bottom: 42 },
2595
+ };
2596
+ },
2597
+ async waitForConversationReady(options = {}) {
2598
+ recorded.push(options.requirePanelsClosed ? "waitForConversationReady:strict" : "waitForConversationReady");
2599
+ return {
2600
+ hasOnlineResume: true,
2601
+ hasAskResume: true,
2602
+ hasAttachmentResume: false,
2603
+ attachmentResumeEnabled: false,
2604
+ resumeModalOpen: false,
2605
+ candidateDetailOpen: true,
2606
+ panelsClosed: false,
2607
+ editorVisible: true,
2608
+ activeSubmit: true,
2609
+ hasAnySubmit: true,
2610
+ messageInputReady: true,
2105
2611
  };
2106
2612
  },
2107
2613
  },
@@ -2109,299 +2615,308 @@ async function testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail() {
2109
2615
 
2110
2616
  const result = await app.processCustomer(
2111
2617
  {
2112
- customerKey: "candidate-dom",
2113
- name: "候选人D",
2114
- school: "浙江大学",
2115
- major: "计算机",
2618
+ customerKey: "candidate-skip-outreach",
2619
+ name: "候选人H",
2116
2620
  sourceJob: "算法工程师",
2117
2621
  domIndex: 0,
2118
- customerId: "1004",
2622
+ customerId: "2002",
2119
2623
  textSnippet: "",
2120
2624
  },
2121
2625
  { screeningCriteria: "有 AI 项目经验" },
2122
- "run-dom",
2123
- { skipCardClick: true },
2124
- );
2125
-
2126
- assert.equal(result.artifacts.resumeAcquisitionMode, "dom_fallback");
2127
- assert.equal(result.artifacts.resumeAcquisitionReason, "dom_retry_hit");
2128
- assert.equal(domReadCount, 2);
2129
- assert.equal(recorded.includes("activateCandidate"), true);
2130
- assert.equal(recorded.includes("openOnlineResume"), true);
2131
- }
2132
-
2133
- async function testBossChatAppShouldPersistEvidenceArtifacts() {
2134
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
2135
- await mkdir(tempDir, { recursive: true });
2136
- const records = [];
2137
- const page = {
2138
- async closeResumeModalDomOnce() {
2139
- return {
2140
- closed: true,
2141
- method: "dom",
2142
- finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
2143
- };
2144
- },
2145
- async waitForConversationReady() {
2146
- return {
2147
- hasOnlineResume: true,
2148
- hasAskResume: true,
2149
- hasAttachmentResume: false,
2150
- attachmentResumeEnabled: false,
2151
- };
2152
- },
2153
- async openOnlineResume() {
2154
- return { clicked: true, detectedOpen: true, by: "dom" };
2155
- },
2156
- async getResumeRateLimitWarning() {
2157
- return { hit: false, text: "" };
2158
- },
2159
- async getResumeProfileFromDom() {
2160
- return {
2161
- ok: true,
2162
- primarySchool: "南京大学",
2163
- schools: ["南京大学"],
2164
- major: "计算机",
2165
- majors: ["计算机"],
2166
- company: "OpenAI",
2167
- position: "工程师",
2168
- resumeText: "南京大学 计算机 PASS_MARKER_ABC",
2169
- evidenceCorpus: "南京大学 计算机 PASS_MARKER_ABC",
2170
- };
2171
- },
2172
- async getResumeModalState() {
2173
- return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
2174
- },
2175
- };
2176
- const llmClient = {
2177
- async evaluateResume() {
2178
- return {
2179
- passed: false,
2180
- rawOutputText: '{"passed":false}',
2181
- rawReasoningText: "先看项目经验,再看技能,结论不通过。",
2182
- cot: "先看项目经验,再看技能,结论不通过。",
2183
- reason: "项目经验与岗位要求不符",
2184
- summary: "不符合要求",
2185
- evidence: ["Python"],
2186
- evaluationMode: "image-multi-chunk",
2187
- imageCount: 3,
2188
- chunkIndex: 1,
2189
- chunkTotal: 1,
2190
- };
2191
- },
2192
- };
2193
- const interaction = {
2194
- async sleepRange() {},
2195
- async clickRect() {},
2196
- };
2197
- const resumeCaptureService = {
2198
- async captureResume({ artifactDir }) {
2199
- return {
2200
- metadataFile: path.join(artifactDir, "chunks.json"),
2201
- chunkDir: path.join(artifactDir, "chunks"),
2202
- chunkCount: 1,
2203
- modelImagePaths: [
2204
- path.join(artifactDir, "chunks", "chunk_000.png"),
2205
- path.join(artifactDir, "chunks", "chunk_001.png"),
2206
- path.join(artifactDir, "chunks", "chunk_002.png"),
2207
- ],
2208
- stitchedImage: "",
2209
- quality: { likelyBlank: false },
2210
- };
2211
- },
2212
- };
2213
- const stateStore = {
2214
- async record(_key, result) {
2215
- records.push(result);
2216
- },
2217
- };
2218
- const app = new BossChatApp({
2219
- page,
2220
- llmClient,
2221
- interaction,
2222
- resumeCaptureService,
2223
- stateStore,
2224
- reportStore: { async write() { return ""; } },
2225
- dryRun: true,
2226
- artifactRootDir: tempDir,
2227
- resumeOpenCooldownMs: 0,
2228
- logger: { log() {} },
2229
- });
2230
- app.waitResumeOpenCooldown = async () => {};
2231
-
2232
- const result = await app.processCustomer(
2233
- {
2234
- customerKey: "candidate-key",
2235
- name: "候选人A",
2236
- sourceJob: "算法工程师",
2237
- domIndex: 0,
2238
- customerId: "1001",
2239
- textSnippet: "",
2240
- },
2241
- {
2242
- screeningCriteria: "有 AI 项目经验",
2243
- },
2244
- "run-test",
2626
+ "run-skip-outreach",
2245
2627
  { skipCardClick: true },
2246
2628
  );
2247
2629
 
2630
+ assert.equal(result.decision, "skipped");
2248
2631
  assert.equal(result.passed, false);
2249
- assert.equal(result.artifacts.finalPassed, false);
2250
- assert.equal(result.reason, "项目经验与岗位要求不符");
2251
- assert.equal(result.artifacts.evaluationMode, "image-multi-chunk");
2252
- assert.equal(result.artifacts.evaluationImageCount, 3);
2253
- assert.equal(result.artifacts.llmReason, "项目经验与岗位要求不符");
2254
- assert.equal(result.artifacts.llmSummary, "不符合要求");
2255
- assert.equal(result.artifacts.llmCot, "先看项目经验,再看技能,结论不通过。");
2256
- assert.deepEqual(result.artifacts.llmEvidence, ["Python"]);
2257
- assert.equal(result.artifacts.llmRawReasoning, "先看项目经验,再看技能,结论不通过。");
2258
- assert.equal(result.artifacts.llmRawOutput, '{"passed":false}');
2259
- assert.equal(Array.isArray(result.artifacts.modelImagePaths), true);
2260
- assert.equal(result.artifacts.modelImagePaths.length, 3);
2261
- assert.equal(Array.isArray(records), true);
2262
- assert.equal(records.length, 1);
2632
+ assert.equal(result.artifacts.finalPassed, true);
2633
+ assert.equal(result.artifacts.preActionResumeClosed, true);
2634
+ assert.equal(result.artifacts.preActionDetailClosed, false);
2635
+ assert.equal(result.artifacts.preActionCleanupRetried, true);
2636
+ assert.equal(result.artifacts.preActionCleanupFailureReason.includes("candidate-detail-still-open"), true);
2637
+ assert.equal(result.artifacts.finalDetailClosed, false);
2638
+ assert.equal(recorded.includes("setEditorMessage"), false);
2639
+ assert.equal(recorded.includes("sendMessage"), false);
2640
+ assert.equal(recorded.includes("clickAskResume"), false);
2263
2641
  }
2264
2642
 
2265
- async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2266
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-report-store-"));
2267
- const reportStore = new ReportStore(tempDir);
2268
- const summary = {
2269
- startedAt: "2026-04-17T10:00:00.000Z",
2270
- finishedAt: "2026-04-17T10:01:05.000Z",
2271
- dryRun: true,
2272
- profile: {
2273
- targetCount: 5,
2274
- screeningCriteria: "有 AI 项目经验",
2275
- },
2276
- inspected: 2,
2277
- passed: 1,
2278
- requested: 0,
2279
- skipped: 1,
2280
- errors: 0,
2281
- exhausted: false,
2282
- stopped: false,
2283
- stopReason: "",
2284
- results: [
2285
- {
2286
- name: "候选人A",
2287
- sourceJob: "算法工程师",
2288
- decision: "passed",
2289
- passed: true,
2290
- requested: false,
2291
- reason: "符合要求",
2292
- error: "",
2293
- artifacts: {
2294
- resumeAcquisitionMode: "network",
2295
- resumeAcquisitionReason: "initial_hit",
2296
- textModelMs: 18234,
2297
- initialNetworkWaitMs: 4200,
2298
- evaluationMode: "text",
2299
- llmSummary: "教育与项目经历匹配",
2300
- llmCot: "先看教育背景,再看项目经历,结论通过。",
2301
- llmEvidence: ["AI Agent", "MCP"],
2302
- llmRawReasoning: "先看教育背景,再看项目经历,结论通过。",
2303
- llmRawOutput: '{"passed":true}',
2304
- },
2305
- },
2306
- {
2307
- name: "候选人B",
2308
- sourceJob: "大模型算法",
2309
- decision: "skipped",
2310
- passed: false,
2311
- requested: false,
2312
- reason: "LLM判定不通过",
2313
- error: "",
2314
- artifacts: {
2315
- resumeAcquisitionMode: "image_fallback",
2316
- resumeAcquisitionReason: "late_network_miss",
2317
- imageCaptureMs: 2300,
2318
- imageModelMs: 19500,
2319
- lateNetworkRetryMs: 3000,
2320
- evaluationMode: "image-multi-chunk",
2321
- evaluationImageCount: 3,
2322
- llmSummary: "项目经历不足",
2323
- llmCot: "先看项目经历,再看实习时长,结论不通过。",
2324
- llmEvidence: ["数据分析"],
2325
- llmRawReasoning: "先看项目经历,再看实习时长,结论不通过。",
2326
- llmRawOutput: '{"passed":false}',
2327
- },
2328
- },
2329
- ],
2330
- reportPath: null,
2331
- };
2332
-
2333
- const jsonPath = await reportStore.write(summary);
2334
- const markdownPath = summary.reportMarkdownPath;
2335
- const csvPath = summary.reportCsvPath;
2336
- const jsonContent = fs.readFileSync(jsonPath, "utf8");
2337
- const markdownContent = fs.readFileSync(markdownPath, "utf8");
2338
- const csvContent = fs.readFileSync(csvPath, "utf8");
2339
-
2340
- assert.equal(path.extname(jsonPath), ".json");
2341
- assert.equal(path.extname(markdownPath), ".md");
2342
- assert.equal(path.extname(csvPath), ".csv");
2343
- assert.equal(summary.reportPath, jsonPath);
2344
- assert.equal(typeof summary.reportArtifacts, "object");
2345
- assert.equal(summary.reportArtifacts.markdownPath, markdownPath);
2346
- assert.equal(summary.reportArtifacts.csvPath, csvPath);
2347
-
2348
- const parsedJson = JSON.parse(jsonContent);
2349
- assert.equal(parsedJson.reportPath, jsonPath);
2350
- assert.equal(parsedJson.reportMarkdownPath, markdownPath);
2351
- assert.equal(parsedJson.reportCsvPath, csvPath);
2352
-
2353
- assert.match(markdownContent, /# Boss Chat 运行报告/);
2354
- assert.match(markdownContent, /Resume Acquisition 汇总/);
2355
- assert.match(markdownContent, /Timing 汇总/);
2356
- assert.match(markdownContent, /候选人A/);
2357
- assert.match(markdownContent, /image_fallback/);
2358
- assert.match(markdownContent, /图片模型 19500ms/);
2359
-
2360
- assert.match(csvContent, /resume_acquisition_mode/);
2361
- assert.match(csvContent, /initial_network_wait_ms/);
2362
- assert.match(csvContent, /late_network_retry_ms/);
2363
- assert.match(csvContent, /llm_summary/);
2364
- assert.match(csvContent, /llm_cot/);
2365
- assert.match(csvContent, /llm_raw_reasoning/);
2366
- assert.match(csvContent, /llm_raw_output/);
2367
- assert.match(csvContent, /候选人B/);
2368
- assert.match(csvContent, /image-multi-chunk/);
2369
- assert.match(csvContent, /先看项目经历,再看实习时长/);
2370
- }
2371
-
2372
- async function main() {
2373
- await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
2374
- await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
2643
+ async function testBossChatAppShouldPersistEvidenceArtifacts() {
2644
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
2645
+ await mkdir(tempDir, { recursive: true });
2646
+ const records = [];
2647
+ const page = {
2648
+ async closeResumeModalDomOnce() {
2649
+ return {
2650
+ closed: true,
2651
+ method: "dom",
2652
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
2653
+ };
2654
+ },
2655
+ async waitForConversationReady() {
2656
+ return {
2657
+ hasOnlineResume: true,
2658
+ hasAskResume: true,
2659
+ hasAttachmentResume: false,
2660
+ attachmentResumeEnabled: false,
2661
+ };
2662
+ },
2663
+ async openOnlineResume() {
2664
+ return { clicked: true, detectedOpen: true, by: "dom" };
2665
+ },
2666
+ async getResumeRateLimitWarning() {
2667
+ return { hit: false, text: "" };
2668
+ },
2669
+ async getResumeProfileFromDom() {
2670
+ return {
2671
+ ok: true,
2672
+ primarySchool: "南京大学",
2673
+ schools: ["南京大学"],
2674
+ major: "计算机",
2675
+ majors: ["计算机"],
2676
+ company: "OpenAI",
2677
+ position: "工程师",
2678
+ resumeText: "南京大学 计算机 PASS_MARKER_ABC",
2679
+ evidenceCorpus: "南京大学 计算机 PASS_MARKER_ABC",
2680
+ };
2681
+ },
2682
+ async getResumeModalState() {
2683
+ return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
2684
+ },
2685
+ };
2686
+ const llmClient = {
2687
+ async evaluateResume() {
2688
+ return {
2689
+ passed: false,
2690
+ rawOutputText: '{"passed":false}',
2691
+ rawReasoningText: "先看项目经验,再看技能,结论不通过。",
2692
+ cot: "先看项目经验,再看技能,结论不通过。",
2693
+ reason: "项目经验与岗位要求不符",
2694
+ summary: "不符合要求",
2695
+ evidence: ["Python"],
2696
+ evaluationMode: "image-multi-chunk",
2697
+ imageCount: 3,
2698
+ chunkIndex: 1,
2699
+ chunkTotal: 1,
2700
+ };
2701
+ },
2702
+ };
2703
+ const interaction = {
2704
+ async sleepRange() {},
2705
+ async clickRect() {},
2706
+ };
2707
+ const resumeCaptureService = {
2708
+ async captureResume({ artifactDir }) {
2709
+ return {
2710
+ metadataFile: path.join(artifactDir, "chunks.json"),
2711
+ chunkDir: path.join(artifactDir, "chunks"),
2712
+ chunkCount: 1,
2713
+ modelImagePaths: [
2714
+ path.join(artifactDir, "chunks", "chunk_000.png"),
2715
+ path.join(artifactDir, "chunks", "chunk_001.png"),
2716
+ path.join(artifactDir, "chunks", "chunk_002.png"),
2717
+ ],
2718
+ stitchedImage: "",
2719
+ quality: { likelyBlank: false },
2720
+ };
2721
+ },
2722
+ };
2723
+ const stateStore = {
2724
+ async record(_key, result) {
2725
+ records.push(result);
2726
+ },
2727
+ };
2728
+ const app = new BossChatApp({
2729
+ page,
2730
+ llmClient,
2731
+ interaction,
2732
+ resumeCaptureService,
2733
+ stateStore,
2734
+ reportStore: { async write() { return ""; } },
2735
+ dryRun: true,
2736
+ artifactRootDir: tempDir,
2737
+ resumeOpenCooldownMs: 0,
2738
+ logger: { log() {} },
2739
+ });
2740
+ app.waitResumeOpenCooldown = async () => {};
2741
+
2742
+ const result = await app.processCustomer(
2743
+ {
2744
+ customerKey: "candidate-key",
2745
+ name: "候选人A",
2746
+ sourceJob: "算法工程师",
2747
+ domIndex: 0,
2748
+ customerId: "1001",
2749
+ textSnippet: "",
2750
+ },
2751
+ {
2752
+ screeningCriteria: "有 AI 项目经验",
2753
+ },
2754
+ "run-test",
2755
+ { skipCardClick: true },
2756
+ );
2757
+
2758
+ assert.equal(result.passed, false);
2759
+ assert.equal(result.artifacts.finalPassed, false);
2760
+ assert.equal(result.reason, "项目经验与岗位要求不符");
2761
+ assert.equal(result.artifacts.evaluationMode, "image-multi-chunk");
2762
+ assert.equal(result.artifacts.evaluationImageCount, 3);
2763
+ assert.equal(result.artifacts.llmReason, "项目经验与岗位要求不符");
2764
+ assert.equal(result.artifacts.llmSummary, "不符合要求");
2765
+ assert.equal(result.artifacts.llmCot, "先看项目经验,再看技能,结论不通过。");
2766
+ assert.deepEqual(result.artifacts.llmEvidence, ["Python"]);
2767
+ assert.equal(result.artifacts.llmRawReasoning, "先看项目经验,再看技能,结论不通过。");
2768
+ assert.equal(result.artifacts.llmRawOutput, '{"passed":false}');
2769
+ assert.equal(Array.isArray(result.artifacts.modelImagePaths), true);
2770
+ assert.equal(result.artifacts.modelImagePaths.length, 3);
2771
+ assert.equal(Array.isArray(records), true);
2772
+ assert.equal(records.length, 1);
2773
+ }
2774
+
2775
+ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2776
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-report-store-"));
2777
+ const reportStore = new ReportStore(tempDir);
2778
+ const summary = {
2779
+ startedAt: "2026-04-17T10:00:00.000Z",
2780
+ finishedAt: "2026-04-17T10:01:05.000Z",
2781
+ dryRun: true,
2782
+ profile: {
2783
+ targetCount: 5,
2784
+ screeningCriteria: "有 AI 项目经验",
2785
+ },
2786
+ inspected: 2,
2787
+ passed: 1,
2788
+ requested: 0,
2789
+ skipped: 1,
2790
+ errors: 0,
2791
+ exhausted: false,
2792
+ stopped: false,
2793
+ stopReason: "",
2794
+ results: [
2795
+ {
2796
+ name: "候选人A",
2797
+ sourceJob: "算法工程师",
2798
+ decision: "passed",
2799
+ passed: true,
2800
+ requested: false,
2801
+ reason: "符合要求",
2802
+ error: "",
2803
+ artifacts: {
2804
+ resumeAcquisitionMode: "network",
2805
+ resumeAcquisitionReason: "initial_hit",
2806
+ textModelMs: 18234,
2807
+ initialNetworkWaitMs: 4200,
2808
+ evaluationMode: "text",
2809
+ llmSummary: "教育与项目经历匹配",
2810
+ llmCot: "先看教育背景,再看项目经历,结论通过。",
2811
+ llmEvidence: ["AI Agent", "MCP"],
2812
+ llmRawReasoning: "先看教育背景,再看项目经历,结论通过。",
2813
+ llmRawOutput: '{"passed":true}',
2814
+ },
2815
+ },
2816
+ {
2817
+ name: "候选人B",
2818
+ sourceJob: "大模型算法",
2819
+ decision: "skipped",
2820
+ passed: false,
2821
+ requested: false,
2822
+ reason: "LLM判定不通过",
2823
+ error: "",
2824
+ artifacts: {
2825
+ resumeAcquisitionMode: "image_fallback",
2826
+ resumeAcquisitionReason: "late_network_miss",
2827
+ imageCaptureMs: 2300,
2828
+ imageModelMs: 19500,
2829
+ lateNetworkRetryMs: 3000,
2830
+ evaluationMode: "image-multi-chunk",
2831
+ evaluationImageCount: 3,
2832
+ llmSummary: "项目经历不足",
2833
+ llmCot: "先看项目经历,再看实习时长,结论不通过。",
2834
+ llmEvidence: ["数据分析"],
2835
+ llmRawReasoning: "先看项目经历,再看实习时长,结论不通过。",
2836
+ llmRawOutput: '{"passed":false}',
2837
+ },
2838
+ },
2839
+ ],
2840
+ reportPath: null,
2841
+ };
2842
+
2843
+ const jsonPath = await reportStore.write(summary);
2844
+ const markdownPath = summary.reportMarkdownPath;
2845
+ const csvPath = summary.reportCsvPath;
2846
+ const jsonContent = fs.readFileSync(jsonPath, "utf8");
2847
+ const markdownContent = fs.readFileSync(markdownPath, "utf8");
2848
+ const csvContent = fs.readFileSync(csvPath, "utf8");
2849
+
2850
+ assert.equal(path.extname(jsonPath), ".json");
2851
+ assert.equal(path.extname(markdownPath), ".md");
2852
+ assert.equal(path.extname(csvPath), ".csv");
2853
+ assert.equal(summary.reportPath, jsonPath);
2854
+ assert.equal(typeof summary.reportArtifacts, "object");
2855
+ assert.equal(summary.reportArtifacts.markdownPath, markdownPath);
2856
+ assert.equal(summary.reportArtifacts.csvPath, csvPath);
2857
+
2858
+ const parsedJson = JSON.parse(jsonContent);
2859
+ assert.equal(parsedJson.reportPath, jsonPath);
2860
+ assert.equal(parsedJson.reportMarkdownPath, markdownPath);
2861
+ assert.equal(parsedJson.reportCsvPath, csvPath);
2862
+
2863
+ assert.match(markdownContent, /# Boss Chat 运行报告/);
2864
+ assert.match(markdownContent, /Resume Acquisition 汇总/);
2865
+ assert.match(markdownContent, /Timing 汇总/);
2866
+ assert.match(markdownContent, /候选人A/);
2867
+ assert.match(markdownContent, /image_fallback/);
2868
+ assert.match(markdownContent, /图片模型 19500ms/);
2869
+
2870
+ assert.match(csvContent, /resume_acquisition_mode/);
2871
+ assert.match(csvContent, /initial_network_wait_ms/);
2872
+ assert.match(csvContent, /late_network_retry_ms/);
2873
+ assert.match(csvContent, /llm_summary/);
2874
+ assert.match(csvContent, /llm_cot/);
2875
+ assert.match(csvContent, /llm_raw_reasoning/);
2876
+ assert.match(csvContent, /llm_raw_output/);
2877
+ assert.match(csvContent, /候选人B/);
2878
+ assert.match(csvContent, /image-multi-chunk/);
2879
+ assert.match(csvContent, /先看项目经历,再看实习时长/);
2880
+ }
2881
+
2882
+ async function main() {
2883
+ await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
2884
+ await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
2375
2885
  await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
2376
2886
  await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
2377
2887
  await testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail();
2888
+ await testChromeClientShouldInjectBrowserHelpersIntoCallFunction();
2889
+ await testBossChatPageShouldWaitForPanelsClosedInStrictConversationReady();
2890
+ await testBossChatPageShouldSurfaceCandidateDetailOverlayAndContentState();
2378
2891
  await testBossChatMcpToolsShouldValidateAndRoute();
2379
- await testBossChatCliShouldSupportRunAndFollowUpParsing();
2380
- await testVendorBossChatCliShouldWaitForHydratedChatShell();
2381
- await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
2382
- testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
2383
- testVendorBossChatCliShouldParseSharedLlmTransportArgs();
2384
- testBossChatLlmParserShouldAcceptMinimalDecisionJson();
2385
- testBossChatLlmParserShouldAcceptPlainPassFailText();
2386
- testBossChatLlmParserShouldAcceptDecisionField();
2387
- testBossChatLlmParserShouldPreserveReasoningFields();
2388
- testBossChatLlmExtractorsShouldReadProviderReasoningFields();
2389
- await testBossChatLlmTextChunkFallbackShouldWork();
2390
- await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
2391
- await testBossChatLlmShouldSendAllImageChunksInSingleRequest();
2392
- await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
2393
- await testBossChatAppShouldCloseCandidateDetailDuringRunCleanup();
2394
- await testBossChatAppShouldRestoreListContextAfterRecovery();
2395
- await testBossChatAppShouldWaitForCandidateListBeforePriming();
2396
- await testBossChatResumeTrackerShouldRetryInitialNetworkWait();
2397
- await testBossChatResumeTrackerShouldUseImageModeGraceWindow();
2892
+ await testBossChatCliShouldSupportRunAndFollowUpParsing();
2893
+ await testVendorBossChatCliShouldWaitForHydratedChatShell();
2894
+ await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
2895
+ testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
2896
+ testVendorBossChatCliShouldParseSharedLlmTransportArgs();
2897
+ testBossChatLlmParserShouldAcceptMinimalDecisionJson();
2898
+ testBossChatLlmParserShouldAcceptPlainPassFailText();
2899
+ testBossChatLlmParserShouldAcceptDecisionField();
2900
+ testBossChatLlmParserShouldPreserveReasoningFields();
2901
+ testBossChatLlmExtractorsShouldReadProviderReasoningFields();
2902
+ await testBossChatLlmTextChunkFallbackShouldWork();
2903
+ await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
2904
+ await testBossChatLlmShouldSendAllImageChunksInSingleRequest();
2905
+ await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
2906
+ await testBossChatAppShouldCloseCandidateDetailDuringRunCleanup();
2907
+ await testBossChatAppShouldRestoreListContextAfterRecovery();
2908
+ await testBossChatAppShouldWaitForCandidateListBeforePriming();
2909
+ await testBossChatResumeTrackerShouldRetryInitialNetworkWait();
2910
+ await testBossChatResumeTrackerShouldUseImageModeGraceWindow();
2398
2911
  await testBossChatAppShouldUseNetworkBeforeImageFallback();
2399
2912
  await testBossChatAppShouldFallbackToImageAfterNetworkMiss();
2400
2913
  await testBossChatAppShouldRetryLateNetworkBeforeDomFallback();
2401
2914
  await testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail();
2915
+ await testBossChatAppShouldGateOutreachUntilPanelsClosed();
2916
+ await testBossChatAppShouldSkipOutreachWhenDetailStillOpenAfterRetry();
2402
2917
  await testBossChatAppShouldPersistEvidenceArtifacts();
2403
- await testBossChatReportStoreShouldWriteReadableMarkdownAndCsv();
2404
- console.log("boss-chat tests passed");
2405
- }
2406
-
2407
- await main();
2918
+ await testBossChatReportStoreShouldWriteReadableMarkdownAndCsv();
2919
+ console.log("boss-chat tests passed");
2920
+ }
2921
+
2922
+ await main();