@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0

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