@reconcrap/boss-recommend-mcp 1.3.32 → 1.3.33

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