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