@reconcrap/boss-recommend-mcp 1.1.2 → 1.1.4
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/package.json +3 -2
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +195 -195
- package/src/adapters.js +1876 -1806
- package/src/index.js +1254 -1254
- package/src/parser.js +19 -28
- package/src/pipeline.js +919 -792
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +163 -163
- package/src/test-index-async.js +236 -236
- package/src/test-parser.js +55 -0
- package/src/test-pipeline.js +103 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +111 -18
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +508 -452
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +245 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +811 -811
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +201 -201
package/src/test-index-async.js
CHANGED
|
@@ -1,236 +1,236 @@
|
|
|
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 { __testables } from "./index.js";
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
handleRequest,
|
|
9
|
-
activeAsyncRuns,
|
|
10
|
-
setRunPipelineImplForTests
|
|
11
|
-
} = __testables;
|
|
12
|
-
|
|
13
|
-
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
14
|
-
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
15
|
-
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
16
|
-
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
17
|
-
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
18
|
-
|
|
19
|
-
function sleep(ms) {
|
|
20
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function makeToolCall(id, name, args = {}) {
|
|
24
|
-
return {
|
|
25
|
-
jsonrpc: "2.0",
|
|
26
|
-
id,
|
|
27
|
-
method: "tools/call",
|
|
28
|
-
params: {
|
|
29
|
-
name,
|
|
30
|
-
arguments: args
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function readToolPayload(response) {
|
|
36
|
-
return response?.result?.structuredContent;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function callTool(name, args, id = 1) {
|
|
40
|
-
const response = await handleRequest(
|
|
41
|
-
makeToolCall(id, name, args),
|
|
42
|
-
process.cwd()
|
|
43
|
-
);
|
|
44
|
-
return readToolPayload(response);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function waitForRunState(runId, acceptedStates, timeoutMs = 6000) {
|
|
48
|
-
const accepted = new Set(acceptedStates.map((item) => String(item).toLowerCase()));
|
|
49
|
-
const deadline = Date.now() + timeoutMs;
|
|
50
|
-
while (Date.now() < deadline) {
|
|
51
|
-
const payload = await callTool(TOOL_GET_RUN, { run_id: runId }, 1001);
|
|
52
|
-
const state = String(payload?.run?.state || "").toLowerCase();
|
|
53
|
-
if (accepted.has(state)) return payload.run;
|
|
54
|
-
await sleep(80);
|
|
55
|
-
}
|
|
56
|
-
throw new Error(`Timed out waiting run state (${Array.from(accepted).join(", ")}) for run_id=${runId}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function startAcceptedRun(instruction, idSeed = 1) {
|
|
60
|
-
const payload = await callTool(TOOL_START_RUN, {
|
|
61
|
-
instruction,
|
|
62
|
-
confirmation: {
|
|
63
|
-
job_confirmed: true,
|
|
64
|
-
job_value: "mock job",
|
|
65
|
-
final_confirmed: true
|
|
66
|
-
}
|
|
67
|
-
}, idSeed);
|
|
68
|
-
assert.equal(payload.status, "ACCEPTED");
|
|
69
|
-
assert.equal(typeof payload.run_id, "string");
|
|
70
|
-
return payload.run_id;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function setupPipelineMock() {
|
|
74
|
-
const checkpointStore = new Map();
|
|
75
|
-
setRunPipelineImplForTests(async (input, _deps, runtime) => {
|
|
76
|
-
if (input.confirmation?.job_confirmed !== true) {
|
|
77
|
-
return {
|
|
78
|
-
status: "NEED_CONFIRMATION",
|
|
79
|
-
required_confirmations: ["job"],
|
|
80
|
-
pending_questions: [{ field: "job", question: "请确认岗位" }]
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (input.confirmation?.final_confirmed !== true) {
|
|
84
|
-
return {
|
|
85
|
-
status: "NEED_CONFIRMATION",
|
|
86
|
-
required_confirmations: ["final_review"],
|
|
87
|
-
pending_questions: [{ field: "final_review", question: "请最终确认" }]
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
runtime?.onStage?.({ stage: "preflight", message: "preflight started" });
|
|
92
|
-
await sleep(30);
|
|
93
|
-
runtime?.onStage?.({ stage: "screen", message: "screen running" });
|
|
94
|
-
|
|
95
|
-
const checkpointPath = String(input.resume?.checkpoint_path || "mem://default");
|
|
96
|
-
const outputCsv = String(input.resume?.output_csv || "C:/tmp/mock.csv");
|
|
97
|
-
const total = 12;
|
|
98
|
-
let processed = input.resume?.resume === true ? Number(checkpointStore.get(checkpointPath) || 0) : 0;
|
|
99
|
-
if (!Number.isInteger(processed) || processed < 0) processed = 0;
|
|
100
|
-
|
|
101
|
-
while (processed < total) {
|
|
102
|
-
processed += 1;
|
|
103
|
-
runtime?.onProgress?.({
|
|
104
|
-
stage: "screen",
|
|
105
|
-
processed,
|
|
106
|
-
passed: Math.floor(processed / 3),
|
|
107
|
-
skipped: processed - Math.floor(processed / 3),
|
|
108
|
-
greet_count: 0,
|
|
109
|
-
line: `处理第 ${processed} 位候选人`
|
|
110
|
-
});
|
|
111
|
-
await sleep(25);
|
|
112
|
-
if (runtime?.isPauseRequested?.() === true) {
|
|
113
|
-
checkpointStore.set(checkpointPath, processed);
|
|
114
|
-
return {
|
|
115
|
-
status: "PAUSED",
|
|
116
|
-
result: {
|
|
117
|
-
processed_count: processed,
|
|
118
|
-
passed_count: Math.floor(processed / 3),
|
|
119
|
-
skipped_count: processed - Math.floor(processed / 3),
|
|
120
|
-
greet_count: 0,
|
|
121
|
-
output_csv: outputCsv,
|
|
122
|
-
checkpoint_path: checkpointPath,
|
|
123
|
-
completion_reason: "paused"
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
checkpointStore.delete(checkpointPath);
|
|
130
|
-
return {
|
|
131
|
-
status: "COMPLETED",
|
|
132
|
-
result: {
|
|
133
|
-
processed_count: total,
|
|
134
|
-
passed_count: Math.floor(total / 3),
|
|
135
|
-
skipped_count: total - Math.floor(total / 3),
|
|
136
|
-
greet_count: 0,
|
|
137
|
-
output_csv: outputCsv,
|
|
138
|
-
completion_reason: "screen_completed"
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function testPauseAndResumeFlow() {
|
|
145
|
-
const runId = await startAcceptedRun("run for pause and resume", 11);
|
|
146
|
-
await waitForRunState(runId, ["running"]);
|
|
147
|
-
|
|
148
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 12);
|
|
149
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
150
|
-
|
|
151
|
-
const pausedRun = await waitForRunState(runId, ["paused"]);
|
|
152
|
-
assert.equal(pausedRun.state, "paused");
|
|
153
|
-
assert.equal(pausedRun.result?.status, "PAUSED");
|
|
154
|
-
assert.equal(Boolean(pausedRun.resume?.output_csv), true);
|
|
155
|
-
|
|
156
|
-
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 13);
|
|
157
|
-
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
158
|
-
assert.equal(resumePayload.run.run_id, runId);
|
|
159
|
-
|
|
160
|
-
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
161
|
-
assert.equal(completedRun.state, "completed");
|
|
162
|
-
assert.equal(completedRun.result?.status, "COMPLETED");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function testResumeAfterProcessRestartSimulation() {
|
|
166
|
-
const runId = await startAcceptedRun("run for restart resume", 21);
|
|
167
|
-
await waitForRunState(runId, ["running"]);
|
|
168
|
-
|
|
169
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 22);
|
|
170
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
171
|
-
await waitForRunState(runId, ["paused"]);
|
|
172
|
-
|
|
173
|
-
// 模拟服务重启后内存态丢失:active map 为空,仅依赖 run-state 持久化恢复。
|
|
174
|
-
activeAsyncRuns.clear();
|
|
175
|
-
|
|
176
|
-
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 23);
|
|
177
|
-
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
178
|
-
assert.equal(resumePayload.run.run_id, runId);
|
|
179
|
-
|
|
180
|
-
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
181
|
-
assert.equal(completedRun.state, "completed");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function testCancelPausedRun() {
|
|
185
|
-
const runId = await startAcceptedRun("run for cancel paused", 31);
|
|
186
|
-
await waitForRunState(runId, ["running"]);
|
|
187
|
-
|
|
188
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 32);
|
|
189
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
190
|
-
await waitForRunState(runId, ["paused"]);
|
|
191
|
-
|
|
192
|
-
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 33);
|
|
193
|
-
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
194
|
-
|
|
195
|
-
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
196
|
-
assert.equal(canceledRun.state, "canceled");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function testCancelRunningRunKeepsCsv() {
|
|
200
|
-
const runId = await startAcceptedRun("run for cancel while running", 41);
|
|
201
|
-
await waitForRunState(runId, ["running"]);
|
|
202
|
-
|
|
203
|
-
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 42);
|
|
204
|
-
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
205
|
-
|
|
206
|
-
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
207
|
-
assert.equal(canceledRun.state, "canceled");
|
|
208
|
-
assert.equal(canceledRun.error?.code, "PIPELINE_CANCELED");
|
|
209
|
-
assert.equal(Boolean(canceledRun.resume?.output_csv), true);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function main() {
|
|
213
|
-
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
214
|
-
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
215
|
-
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
216
|
-
setupPipelineMock();
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
await testPauseAndResumeFlow();
|
|
220
|
-
await testResumeAfterProcessRestartSimulation();
|
|
221
|
-
await testCancelPausedRun();
|
|
222
|
-
await testCancelRunningRunKeepsCsv();
|
|
223
|
-
console.log("index async tests passed");
|
|
224
|
-
} finally {
|
|
225
|
-
setRunPipelineImplForTests(null);
|
|
226
|
-
activeAsyncRuns.clear();
|
|
227
|
-
if (previousHome === undefined) {
|
|
228
|
-
delete process.env.BOSS_RECOMMEND_HOME;
|
|
229
|
-
} else {
|
|
230
|
-
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
231
|
-
}
|
|
232
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
await main();
|
|
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 { __testables } from "./index.js";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
handleRequest,
|
|
9
|
+
activeAsyncRuns,
|
|
10
|
+
setRunPipelineImplForTests
|
|
11
|
+
} = __testables;
|
|
12
|
+
|
|
13
|
+
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
14
|
+
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
15
|
+
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
16
|
+
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
17
|
+
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeToolCall(id, name, args = {}) {
|
|
24
|
+
return {
|
|
25
|
+
jsonrpc: "2.0",
|
|
26
|
+
id,
|
|
27
|
+
method: "tools/call",
|
|
28
|
+
params: {
|
|
29
|
+
name,
|
|
30
|
+
arguments: args
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readToolPayload(response) {
|
|
36
|
+
return response?.result?.structuredContent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function callTool(name, args, id = 1) {
|
|
40
|
+
const response = await handleRequest(
|
|
41
|
+
makeToolCall(id, name, args),
|
|
42
|
+
process.cwd()
|
|
43
|
+
);
|
|
44
|
+
return readToolPayload(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function waitForRunState(runId, acceptedStates, timeoutMs = 6000) {
|
|
48
|
+
const accepted = new Set(acceptedStates.map((item) => String(item).toLowerCase()));
|
|
49
|
+
const deadline = Date.now() + timeoutMs;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
const payload = await callTool(TOOL_GET_RUN, { run_id: runId }, 1001);
|
|
52
|
+
const state = String(payload?.run?.state || "").toLowerCase();
|
|
53
|
+
if (accepted.has(state)) return payload.run;
|
|
54
|
+
await sleep(80);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Timed out waiting run state (${Array.from(accepted).join(", ")}) for run_id=${runId}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function startAcceptedRun(instruction, idSeed = 1) {
|
|
60
|
+
const payload = await callTool(TOOL_START_RUN, {
|
|
61
|
+
instruction,
|
|
62
|
+
confirmation: {
|
|
63
|
+
job_confirmed: true,
|
|
64
|
+
job_value: "mock job",
|
|
65
|
+
final_confirmed: true
|
|
66
|
+
}
|
|
67
|
+
}, idSeed);
|
|
68
|
+
assert.equal(payload.status, "ACCEPTED");
|
|
69
|
+
assert.equal(typeof payload.run_id, "string");
|
|
70
|
+
return payload.run_id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setupPipelineMock() {
|
|
74
|
+
const checkpointStore = new Map();
|
|
75
|
+
setRunPipelineImplForTests(async (input, _deps, runtime) => {
|
|
76
|
+
if (input.confirmation?.job_confirmed !== true) {
|
|
77
|
+
return {
|
|
78
|
+
status: "NEED_CONFIRMATION",
|
|
79
|
+
required_confirmations: ["job"],
|
|
80
|
+
pending_questions: [{ field: "job", question: "请确认岗位" }]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (input.confirmation?.final_confirmed !== true) {
|
|
84
|
+
return {
|
|
85
|
+
status: "NEED_CONFIRMATION",
|
|
86
|
+
required_confirmations: ["final_review"],
|
|
87
|
+
pending_questions: [{ field: "final_review", question: "请最终确认" }]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
runtime?.onStage?.({ stage: "preflight", message: "preflight started" });
|
|
92
|
+
await sleep(30);
|
|
93
|
+
runtime?.onStage?.({ stage: "screen", message: "screen running" });
|
|
94
|
+
|
|
95
|
+
const checkpointPath = String(input.resume?.checkpoint_path || "mem://default");
|
|
96
|
+
const outputCsv = String(input.resume?.output_csv || "C:/tmp/mock.csv");
|
|
97
|
+
const total = 12;
|
|
98
|
+
let processed = input.resume?.resume === true ? Number(checkpointStore.get(checkpointPath) || 0) : 0;
|
|
99
|
+
if (!Number.isInteger(processed) || processed < 0) processed = 0;
|
|
100
|
+
|
|
101
|
+
while (processed < total) {
|
|
102
|
+
processed += 1;
|
|
103
|
+
runtime?.onProgress?.({
|
|
104
|
+
stage: "screen",
|
|
105
|
+
processed,
|
|
106
|
+
passed: Math.floor(processed / 3),
|
|
107
|
+
skipped: processed - Math.floor(processed / 3),
|
|
108
|
+
greet_count: 0,
|
|
109
|
+
line: `处理第 ${processed} 位候选人`
|
|
110
|
+
});
|
|
111
|
+
await sleep(25);
|
|
112
|
+
if (runtime?.isPauseRequested?.() === true) {
|
|
113
|
+
checkpointStore.set(checkpointPath, processed);
|
|
114
|
+
return {
|
|
115
|
+
status: "PAUSED",
|
|
116
|
+
result: {
|
|
117
|
+
processed_count: processed,
|
|
118
|
+
passed_count: Math.floor(processed / 3),
|
|
119
|
+
skipped_count: processed - Math.floor(processed / 3),
|
|
120
|
+
greet_count: 0,
|
|
121
|
+
output_csv: outputCsv,
|
|
122
|
+
checkpoint_path: checkpointPath,
|
|
123
|
+
completion_reason: "paused"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
checkpointStore.delete(checkpointPath);
|
|
130
|
+
return {
|
|
131
|
+
status: "COMPLETED",
|
|
132
|
+
result: {
|
|
133
|
+
processed_count: total,
|
|
134
|
+
passed_count: Math.floor(total / 3),
|
|
135
|
+
skipped_count: total - Math.floor(total / 3),
|
|
136
|
+
greet_count: 0,
|
|
137
|
+
output_csv: outputCsv,
|
|
138
|
+
completion_reason: "screen_completed"
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function testPauseAndResumeFlow() {
|
|
145
|
+
const runId = await startAcceptedRun("run for pause and resume", 11);
|
|
146
|
+
await waitForRunState(runId, ["running"]);
|
|
147
|
+
|
|
148
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 12);
|
|
149
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
150
|
+
|
|
151
|
+
const pausedRun = await waitForRunState(runId, ["paused"]);
|
|
152
|
+
assert.equal(pausedRun.state, "paused");
|
|
153
|
+
assert.equal(pausedRun.result?.status, "PAUSED");
|
|
154
|
+
assert.equal(Boolean(pausedRun.resume?.output_csv), true);
|
|
155
|
+
|
|
156
|
+
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 13);
|
|
157
|
+
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
158
|
+
assert.equal(resumePayload.run.run_id, runId);
|
|
159
|
+
|
|
160
|
+
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
161
|
+
assert.equal(completedRun.state, "completed");
|
|
162
|
+
assert.equal(completedRun.result?.status, "COMPLETED");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function testResumeAfterProcessRestartSimulation() {
|
|
166
|
+
const runId = await startAcceptedRun("run for restart resume", 21);
|
|
167
|
+
await waitForRunState(runId, ["running"]);
|
|
168
|
+
|
|
169
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 22);
|
|
170
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
171
|
+
await waitForRunState(runId, ["paused"]);
|
|
172
|
+
|
|
173
|
+
// 模拟服务重启后内存态丢失:active map 为空,仅依赖 run-state 持久化恢复。
|
|
174
|
+
activeAsyncRuns.clear();
|
|
175
|
+
|
|
176
|
+
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 23);
|
|
177
|
+
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
178
|
+
assert.equal(resumePayload.run.run_id, runId);
|
|
179
|
+
|
|
180
|
+
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
181
|
+
assert.equal(completedRun.state, "completed");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function testCancelPausedRun() {
|
|
185
|
+
const runId = await startAcceptedRun("run for cancel paused", 31);
|
|
186
|
+
await waitForRunState(runId, ["running"]);
|
|
187
|
+
|
|
188
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 32);
|
|
189
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
190
|
+
await waitForRunState(runId, ["paused"]);
|
|
191
|
+
|
|
192
|
+
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 33);
|
|
193
|
+
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
194
|
+
|
|
195
|
+
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
196
|
+
assert.equal(canceledRun.state, "canceled");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function testCancelRunningRunKeepsCsv() {
|
|
200
|
+
const runId = await startAcceptedRun("run for cancel while running", 41);
|
|
201
|
+
await waitForRunState(runId, ["running"]);
|
|
202
|
+
|
|
203
|
+
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 42);
|
|
204
|
+
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
205
|
+
|
|
206
|
+
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
207
|
+
assert.equal(canceledRun.state, "canceled");
|
|
208
|
+
assert.equal(canceledRun.error?.code, "PIPELINE_CANCELED");
|
|
209
|
+
assert.equal(Boolean(canceledRun.resume?.output_csv), true);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function main() {
|
|
213
|
+
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
214
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
215
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
216
|
+
setupPipelineMock();
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await testPauseAndResumeFlow();
|
|
220
|
+
await testResumeAfterProcessRestartSimulation();
|
|
221
|
+
await testCancelPausedRun();
|
|
222
|
+
await testCancelRunningRunKeepsCsv();
|
|
223
|
+
console.log("index async tests passed");
|
|
224
|
+
} finally {
|
|
225
|
+
setRunPipelineImplForTests(null);
|
|
226
|
+
activeAsyncRuns.clear();
|
|
227
|
+
if (previousHome === undefined) {
|
|
228
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
229
|
+
} else {
|
|
230
|
+
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
231
|
+
}
|
|
232
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await main();
|
package/src/test-parser.js
CHANGED
|
@@ -29,9 +29,13 @@ function testConfirmedPostActionAndOverrides() {
|
|
|
29
29
|
confirmation: {
|
|
30
30
|
filters_confirmed: true,
|
|
31
31
|
school_tag_confirmed: true,
|
|
32
|
+
school_tag_value: ["211"],
|
|
32
33
|
degree_confirmed: true,
|
|
34
|
+
degree_value: ["本科"],
|
|
33
35
|
gender_confirmed: true,
|
|
36
|
+
gender_value: "女",
|
|
34
37
|
recent_not_view_confirmed: true,
|
|
38
|
+
recent_not_view_value: "近14天没有",
|
|
35
39
|
criteria_confirmed: true,
|
|
36
40
|
target_count_confirmed: true,
|
|
37
41
|
target_count_value: 12,
|
|
@@ -67,6 +71,30 @@ function testConfirmedPostActionAndOverrides() {
|
|
|
67
71
|
assert.equal(result.needs_max_greet_count_confirmation, false);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
function testMissingRecentNotViewValueShouldRequireReconfirmation() {
|
|
75
|
+
const result = parseRecommendInstruction({
|
|
76
|
+
instruction: "推荐页筛选985男生,近14天没有,有销售经验,符合标准收藏",
|
|
77
|
+
confirmation: {
|
|
78
|
+
filters_confirmed: true,
|
|
79
|
+
school_tag_confirmed: true,
|
|
80
|
+
school_tag_value: ["985"],
|
|
81
|
+
degree_confirmed: true,
|
|
82
|
+
degree_value: ["本科"],
|
|
83
|
+
gender_confirmed: true,
|
|
84
|
+
gender_value: "男",
|
|
85
|
+
recent_not_view_confirmed: true,
|
|
86
|
+
criteria_confirmed: true,
|
|
87
|
+
target_count_confirmed: true,
|
|
88
|
+
post_action_confirmed: true,
|
|
89
|
+
post_action_value: "favorite"
|
|
90
|
+
},
|
|
91
|
+
overrides: null
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.equal(result.needs_recent_not_view_confirmation, true);
|
|
95
|
+
assert.equal(result.pending_questions.some((q) => q.field === "recent_not_view"), true);
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
function testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation() {
|
|
71
99
|
const result = parseRecommendInstruction({
|
|
72
100
|
instruction: "通过boss推荐skill帮我找人",
|
|
@@ -401,6 +429,31 @@ function testTargetCountCanBeSkippedAfterConfirmation() {
|
|
|
401
429
|
assert.equal(result.screenParams.target_count, null);
|
|
402
430
|
}
|
|
403
431
|
|
|
432
|
+
function testPostActionNoneCanBeConfirmed() {
|
|
433
|
+
const result = parseRecommendInstruction({
|
|
434
|
+
instruction: "推荐页筛选211女生,近14天没有,有AI经验,符合标准什么也不做",
|
|
435
|
+
confirmation: {
|
|
436
|
+
filters_confirmed: true,
|
|
437
|
+
school_tag_confirmed: true,
|
|
438
|
+
school_tag_value: ["211"],
|
|
439
|
+
degree_confirmed: true,
|
|
440
|
+
degree_value: ["本科"],
|
|
441
|
+
gender_confirmed: true,
|
|
442
|
+
gender_value: "女",
|
|
443
|
+
recent_not_view_confirmed: true,
|
|
444
|
+
recent_not_view_value: "近14天没有",
|
|
445
|
+
criteria_confirmed: true,
|
|
446
|
+
target_count_confirmed: true,
|
|
447
|
+
post_action_confirmed: true,
|
|
448
|
+
post_action_value: "none"
|
|
449
|
+
},
|
|
450
|
+
overrides: null
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
assert.equal(result.screenParams.post_action, "none");
|
|
454
|
+
assert.equal(result.needs_post_action_confirmation, false);
|
|
455
|
+
}
|
|
456
|
+
|
|
404
457
|
function testJobSelectionHintCanComeFromOverrides() {
|
|
405
458
|
const result = parseRecommendInstruction({
|
|
406
459
|
instruction: "推荐页筛选211女生,有算法经验,符合标准收藏",
|
|
@@ -426,6 +479,7 @@ function testMcpMentionShouldStayInCriteria() {
|
|
|
426
479
|
function main() {
|
|
427
480
|
testNeedConfirmationIncludesPostAction();
|
|
428
481
|
testConfirmedPostActionAndOverrides();
|
|
482
|
+
testMissingRecentNotViewValueShouldRequireReconfirmation();
|
|
429
483
|
testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation();
|
|
430
484
|
testFilterConfirmedWithExplicitConfirmationValuesShouldNotFallbackToUnlimited();
|
|
431
485
|
testMultipleSchoolTagsMarkedSuspicious();
|
|
@@ -445,6 +499,7 @@ function main() {
|
|
|
445
499
|
testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation();
|
|
446
500
|
testTargetCountNeedsConfirmationEvenWhenOptional();
|
|
447
501
|
testTargetCountCanBeSkippedAfterConfirmation();
|
|
502
|
+
testPostActionNoneCanBeConfirmed();
|
|
448
503
|
testJobSelectionHintCanComeFromOverrides();
|
|
449
504
|
console.log("parser tests passed");
|
|
450
505
|
}
|
package/src/test-pipeline.js
CHANGED
|
@@ -252,6 +252,108 @@ async function testResumeFromPausedBeforeScreenShouldRerunSearch() {
|
|
|
252
252
|
assert.equal(result.result.candidate_count, 9);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
async function testConsecutiveResumeCaptureFailuresShouldRefreshAndRerunSearchWithForcedRecentFilter() {
|
|
256
|
+
const searchCalls = [];
|
|
257
|
+
const screenCalls = [];
|
|
258
|
+
let reloadCalls = 0;
|
|
259
|
+
let pageReadyCalls = 0;
|
|
260
|
+
const parsed = createParsed();
|
|
261
|
+
parsed.searchParams = {
|
|
262
|
+
...parsed.searchParams,
|
|
263
|
+
recent_not_view: "不限"
|
|
264
|
+
};
|
|
265
|
+
const result = await runRecommendPipeline(
|
|
266
|
+
{
|
|
267
|
+
workspaceRoot: process.cwd(),
|
|
268
|
+
instruction: "test",
|
|
269
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
270
|
+
overrides: {},
|
|
271
|
+
resume: {
|
|
272
|
+
resume: false,
|
|
273
|
+
output_csv: "C:/temp/resume.csv",
|
|
274
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
275
|
+
pause_control_path: "C:/temp/run.json",
|
|
276
|
+
previous_completion_reason: ""
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
parseRecommendInstruction: () => parsed,
|
|
281
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
282
|
+
ensureBossRecommendPageReady: async () => {
|
|
283
|
+
pageReadyCalls += 1;
|
|
284
|
+
return { ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } };
|
|
285
|
+
},
|
|
286
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
287
|
+
reloadBossRecommendPage: async () => {
|
|
288
|
+
reloadCalls += 1;
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
state: "RECOMMEND_READY",
|
|
292
|
+
reloaded_url: "https://www.zhipin.com/web/chat/recommend"
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
runRecommendSearchCli: async ({ searchParams, selectedJob }) => {
|
|
296
|
+
searchCalls.push({
|
|
297
|
+
searchParams,
|
|
298
|
+
selectedJob
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
summary: {
|
|
303
|
+
candidate_count: 9,
|
|
304
|
+
applied_filters: searchParams,
|
|
305
|
+
selected_job: selectedJob,
|
|
306
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
runRecommendScreenCli: async ({ resume }) => {
|
|
311
|
+
screenCalls.push(resume);
|
|
312
|
+
if (screenCalls.length === 1) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: {
|
|
316
|
+
code: "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT",
|
|
317
|
+
message: "连续 10 位候选人截图失败"
|
|
318
|
+
},
|
|
319
|
+
summary: {
|
|
320
|
+
processed_count: 216,
|
|
321
|
+
passed_count: 83,
|
|
322
|
+
skipped_count: 133,
|
|
323
|
+
output_csv: "C:/temp/resume.csv"
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
summary: {
|
|
330
|
+
processed_count: 240,
|
|
331
|
+
passed_count: 90,
|
|
332
|
+
skipped_count: 150,
|
|
333
|
+
output_csv: "C:/temp/resume.csv",
|
|
334
|
+
completion_reason: "page_exhausted"
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
assert.equal(result.status, "COMPLETED");
|
|
342
|
+
assert.equal(searchCalls.length, 2);
|
|
343
|
+
assert.equal(searchCalls[0].searchParams.recent_not_view, "不限");
|
|
344
|
+
assert.equal(searchCalls[1].searchParams.recent_not_view, "近14天没有");
|
|
345
|
+
assert.equal(screenCalls.length, 2);
|
|
346
|
+
assert.equal(screenCalls[0].resume, false);
|
|
347
|
+
assert.equal(screenCalls[1].resume, true);
|
|
348
|
+
assert.equal(screenCalls[1].require_checkpoint, true);
|
|
349
|
+
assert.equal(screenCalls[1].output_csv, "C:/temp/resume.csv");
|
|
350
|
+
assert.equal(reloadCalls, 1);
|
|
351
|
+
assert.equal(pageReadyCalls, 2);
|
|
352
|
+
assert.equal(result.result.output_csv, "C:/temp/resume.csv");
|
|
353
|
+
assert.equal(result.result.auto_recovery.reload.ok, true);
|
|
354
|
+
assert.equal(result.search_params.recent_not_view, "近14天没有");
|
|
355
|
+
}
|
|
356
|
+
|
|
255
357
|
async function testNeedConfirmationGate() {
|
|
256
358
|
let preflightCalled = false;
|
|
257
359
|
const result = await runRecommendPipeline(
|
|
@@ -933,6 +1035,7 @@ async function main() {
|
|
|
933
1035
|
await testPausedScreenResultShouldBubbleUp();
|
|
934
1036
|
await testResumeFromScreenPauseShouldSkipSearch();
|
|
935
1037
|
await testResumeFromPausedBeforeScreenShouldRerunSearch();
|
|
1038
|
+
await testConsecutiveResumeCaptureFailuresShouldRefreshAndRerunSearchWithForcedRecentFilter();
|
|
936
1039
|
await testNeedConfirmationGate();
|
|
937
1040
|
await testNeedSchoolTagConfirmationGate();
|
|
938
1041
|
await testNeedTargetCountConfirmationGate();
|