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