@reconcrap/boss-recommend-mcp 1.2.4 → 1.2.6
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/bin/boss-recommend-mcp.js +2 -2
- package/config/screening-config.example.json +7 -7
- package/package.json +62 -62
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +244 -244
- package/src/adapters.js +293 -38
- package/src/pipeline.js +1453 -1432
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +469 -469
- package/src/test-index-async.js +264 -264
- package/src/test-pipeline.js +67 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +4052 -3721
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +141 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +880 -832
- package/vendor/boss-recommend-search-cli/src/cli.js +1650 -1650
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +211 -211
package/src/test-index-async.js
CHANGED
|
@@ -1,264 +1,264 @@
|
|
|
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
|
-
runDetachedWorkerForTests,
|
|
10
|
-
setSpawnProcessImplForTests,
|
|
11
|
-
setRunPipelineImplForTests
|
|
12
|
-
} = __testables;
|
|
13
|
-
|
|
14
|
-
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
15
|
-
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
16
|
-
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
17
|
-
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
18
|
-
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
19
|
-
|
|
20
|
-
function sleep(ms) {
|
|
21
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeToolCall(id, name, args = {}) {
|
|
25
|
-
return {
|
|
26
|
-
jsonrpc: "2.0",
|
|
27
|
-
id,
|
|
28
|
-
method: "tools/call",
|
|
29
|
-
params: {
|
|
30
|
-
name,
|
|
31
|
-
arguments: args
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function readToolPayload(response) {
|
|
37
|
-
return response?.result?.structuredContent;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function callTool(name, args, id = 1) {
|
|
41
|
-
const response = await handleRequest(
|
|
42
|
-
makeToolCall(id, name, args),
|
|
43
|
-
process.cwd()
|
|
44
|
-
);
|
|
45
|
-
return readToolPayload(response);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function waitForRunState(runId, acceptedStates, timeoutMs = 6000) {
|
|
49
|
-
const accepted = new Set(acceptedStates.map((item) => String(item).toLowerCase()));
|
|
50
|
-
const deadline = Date.now() + timeoutMs;
|
|
51
|
-
while (Date.now() < deadline) {
|
|
52
|
-
const payload = await callTool(TOOL_GET_RUN, { run_id: runId }, 1001);
|
|
53
|
-
const state = String(payload?.run?.state || "").toLowerCase();
|
|
54
|
-
if (accepted.has(state)) return payload.run;
|
|
55
|
-
await sleep(80);
|
|
56
|
-
}
|
|
57
|
-
throw new Error(`Timed out waiting run state (${Array.from(accepted).join(", ")}) for run_id=${runId}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function startAcceptedRun(instruction, idSeed = 1) {
|
|
61
|
-
const payload = await callTool(TOOL_START_RUN, {
|
|
62
|
-
instruction,
|
|
63
|
-
confirmation: {
|
|
64
|
-
job_confirmed: true,
|
|
65
|
-
job_value: "mock job",
|
|
66
|
-
final_confirmed: true
|
|
67
|
-
}
|
|
68
|
-
}, idSeed);
|
|
69
|
-
assert.equal(payload.status, "ACCEPTED");
|
|
70
|
-
assert.equal(typeof payload.run_id, "string");
|
|
71
|
-
return payload.run_id;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function setupPipelineMock() {
|
|
75
|
-
const checkpointStore = new Map();
|
|
76
|
-
setRunPipelineImplForTests(async (input, _deps, runtime) => {
|
|
77
|
-
if (input.confirmation?.job_confirmed !== true) {
|
|
78
|
-
return {
|
|
79
|
-
status: "NEED_CONFIRMATION",
|
|
80
|
-
required_confirmations: ["job"],
|
|
81
|
-
pending_questions: [{ field: "job", question: "请确认岗位" }]
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (input.confirmation?.final_confirmed !== true) {
|
|
85
|
-
return {
|
|
86
|
-
status: "NEED_CONFIRMATION",
|
|
87
|
-
required_confirmations: ["final_review"],
|
|
88
|
-
pending_questions: [{ field: "final_review", question: "请最终确认" }]
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
runtime?.onStage?.({ stage: "preflight", message: "preflight started" });
|
|
93
|
-
await sleep(30);
|
|
94
|
-
runtime?.onStage?.({ stage: "screen", message: "screen running" });
|
|
95
|
-
|
|
96
|
-
const checkpointPath = String(input.resume?.checkpoint_path || "mem://default");
|
|
97
|
-
const outputCsv = String(input.resume?.output_csv || "C:/tmp/mock.csv");
|
|
98
|
-
const total = 12;
|
|
99
|
-
let processed = input.resume?.resume === true ? Number(checkpointStore.get(checkpointPath) || 0) : 0;
|
|
100
|
-
if (!Number.isInteger(processed) || processed < 0) processed = 0;
|
|
101
|
-
|
|
102
|
-
while (processed < total) {
|
|
103
|
-
processed += 1;
|
|
104
|
-
runtime?.onProgress?.({
|
|
105
|
-
stage: "screen",
|
|
106
|
-
processed,
|
|
107
|
-
passed: Math.floor(processed / 3),
|
|
108
|
-
skipped: processed - Math.floor(processed / 3),
|
|
109
|
-
greet_count: 0,
|
|
110
|
-
line: `处理第 ${processed} 位候选人`
|
|
111
|
-
});
|
|
112
|
-
await sleep(25);
|
|
113
|
-
if (runtime?.isPauseRequested?.() === true) {
|
|
114
|
-
checkpointStore.set(checkpointPath, processed);
|
|
115
|
-
return {
|
|
116
|
-
status: "PAUSED",
|
|
117
|
-
result: {
|
|
118
|
-
processed_count: processed,
|
|
119
|
-
passed_count: Math.floor(processed / 3),
|
|
120
|
-
skipped_count: processed - Math.floor(processed / 3),
|
|
121
|
-
greet_count: 0,
|
|
122
|
-
output_csv: outputCsv,
|
|
123
|
-
checkpoint_path: checkpointPath,
|
|
124
|
-
completion_reason: "paused"
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
checkpointStore.delete(checkpointPath);
|
|
131
|
-
return {
|
|
132
|
-
status: "COMPLETED",
|
|
133
|
-
result: {
|
|
134
|
-
processed_count: total,
|
|
135
|
-
passed_count: Math.floor(total / 3),
|
|
136
|
-
skipped_count: total - Math.floor(total / 3),
|
|
137
|
-
greet_count: 0,
|
|
138
|
-
output_csv: outputCsv,
|
|
139
|
-
completion_reason: "screen_completed"
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function parseDetachedSpawnArgs(argv = []) {
|
|
146
|
-
const normalized = Array.isArray(argv) ? argv.map((item) => String(item || "")) : [];
|
|
147
|
-
const runIdFlagIndex = normalized.indexOf("--run-id");
|
|
148
|
-
return {
|
|
149
|
-
runId: runIdFlagIndex >= 0 ? String(normalized[runIdFlagIndex + 1] || "").trim() : "",
|
|
150
|
-
resumeRun: normalized.includes("--resume")
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function setupDetachedWorkerStub() {
|
|
155
|
-
setSpawnProcessImplForTests((command, argv = []) => {
|
|
156
|
-
assert.equal(typeof command, "string");
|
|
157
|
-
const { runId, resumeRun } = parseDetachedSpawnArgs(argv);
|
|
158
|
-
assert.equal(Boolean(runId), true, "detached worker spawn must include --run-id");
|
|
159
|
-
const pid = process.pid;
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
runDetachedWorkerForTests({
|
|
162
|
-
runId,
|
|
163
|
-
resumeRun,
|
|
164
|
-
workerPid: pid
|
|
165
|
-
}).catch(() => {});
|
|
166
|
-
}, 0);
|
|
167
|
-
return {
|
|
168
|
-
pid,
|
|
169
|
-
unref() {}
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function testPauseAndResumeFlow() {
|
|
175
|
-
const runId = await startAcceptedRun("run for pause and resume", 11);
|
|
176
|
-
await waitForRunState(runId, ["running"]);
|
|
177
|
-
|
|
178
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 12);
|
|
179
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
180
|
-
|
|
181
|
-
const pausedRun = await waitForRunState(runId, ["paused"]);
|
|
182
|
-
assert.equal(pausedRun.state, "paused");
|
|
183
|
-
assert.equal(pausedRun.result?.status, "PAUSED");
|
|
184
|
-
assert.equal(Boolean(pausedRun.resume?.output_csv), true);
|
|
185
|
-
|
|
186
|
-
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 13);
|
|
187
|
-
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
188
|
-
assert.equal(resumePayload.run.run_id, runId);
|
|
189
|
-
|
|
190
|
-
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
191
|
-
assert.equal(completedRun.state, "completed");
|
|
192
|
-
assert.equal(completedRun.result?.status, "COMPLETED");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function testResumeAfterProcessRestartSimulation() {
|
|
196
|
-
const runId = await startAcceptedRun("run for restart resume", 21);
|
|
197
|
-
await waitForRunState(runId, ["running"]);
|
|
198
|
-
|
|
199
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 22);
|
|
200
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
201
|
-
await waitForRunState(runId, ["paused"]);
|
|
202
|
-
|
|
203
|
-
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 23);
|
|
204
|
-
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
205
|
-
assert.equal(resumePayload.run.run_id, runId);
|
|
206
|
-
|
|
207
|
-
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
208
|
-
assert.equal(completedRun.state, "completed");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function testCancelPausedRun() {
|
|
212
|
-
const runId = await startAcceptedRun("run for cancel paused", 31);
|
|
213
|
-
await waitForRunState(runId, ["running"]);
|
|
214
|
-
|
|
215
|
-
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 32);
|
|
216
|
-
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
217
|
-
await waitForRunState(runId, ["paused"]);
|
|
218
|
-
|
|
219
|
-
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 33);
|
|
220
|
-
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
221
|
-
|
|
222
|
-
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
223
|
-
assert.equal(canceledRun.state, "canceled");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function testCancelRunningRunKeepsCsv() {
|
|
227
|
-
const runId = await startAcceptedRun("run for cancel while running", 41);
|
|
228
|
-
await waitForRunState(runId, ["running"]);
|
|
229
|
-
|
|
230
|
-
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 42);
|
|
231
|
-
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
232
|
-
|
|
233
|
-
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
234
|
-
assert.equal(canceledRun.state, "canceled");
|
|
235
|
-
assert.equal(canceledRun.error?.code, "PIPELINE_CANCELED");
|
|
236
|
-
assert.equal(Boolean(canceledRun.resume?.output_csv), true);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function main() {
|
|
240
|
-
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
241
|
-
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
242
|
-
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
243
|
-
setupPipelineMock();
|
|
244
|
-
setupDetachedWorkerStub();
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
await testPauseAndResumeFlow();
|
|
248
|
-
await testResumeAfterProcessRestartSimulation();
|
|
249
|
-
await testCancelPausedRun();
|
|
250
|
-
await testCancelRunningRunKeepsCsv();
|
|
251
|
-
console.log("index async tests passed");
|
|
252
|
-
} finally {
|
|
253
|
-
setRunPipelineImplForTests(null);
|
|
254
|
-
setSpawnProcessImplForTests(null);
|
|
255
|
-
if (previousHome === undefined) {
|
|
256
|
-
delete process.env.BOSS_RECOMMEND_HOME;
|
|
257
|
-
} else {
|
|
258
|
-
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
259
|
-
}
|
|
260
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
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
|
+
runDetachedWorkerForTests,
|
|
10
|
+
setSpawnProcessImplForTests,
|
|
11
|
+
setRunPipelineImplForTests
|
|
12
|
+
} = __testables;
|
|
13
|
+
|
|
14
|
+
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
15
|
+
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
16
|
+
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
17
|
+
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
18
|
+
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
19
|
+
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeToolCall(id, name, args = {}) {
|
|
25
|
+
return {
|
|
26
|
+
jsonrpc: "2.0",
|
|
27
|
+
id,
|
|
28
|
+
method: "tools/call",
|
|
29
|
+
params: {
|
|
30
|
+
name,
|
|
31
|
+
arguments: args
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readToolPayload(response) {
|
|
37
|
+
return response?.result?.structuredContent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function callTool(name, args, id = 1) {
|
|
41
|
+
const response = await handleRequest(
|
|
42
|
+
makeToolCall(id, name, args),
|
|
43
|
+
process.cwd()
|
|
44
|
+
);
|
|
45
|
+
return readToolPayload(response);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function waitForRunState(runId, acceptedStates, timeoutMs = 6000) {
|
|
49
|
+
const accepted = new Set(acceptedStates.map((item) => String(item).toLowerCase()));
|
|
50
|
+
const deadline = Date.now() + timeoutMs;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
const payload = await callTool(TOOL_GET_RUN, { run_id: runId }, 1001);
|
|
53
|
+
const state = String(payload?.run?.state || "").toLowerCase();
|
|
54
|
+
if (accepted.has(state)) return payload.run;
|
|
55
|
+
await sleep(80);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Timed out waiting run state (${Array.from(accepted).join(", ")}) for run_id=${runId}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function startAcceptedRun(instruction, idSeed = 1) {
|
|
61
|
+
const payload = await callTool(TOOL_START_RUN, {
|
|
62
|
+
instruction,
|
|
63
|
+
confirmation: {
|
|
64
|
+
job_confirmed: true,
|
|
65
|
+
job_value: "mock job",
|
|
66
|
+
final_confirmed: true
|
|
67
|
+
}
|
|
68
|
+
}, idSeed);
|
|
69
|
+
assert.equal(payload.status, "ACCEPTED");
|
|
70
|
+
assert.equal(typeof payload.run_id, "string");
|
|
71
|
+
return payload.run_id;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setupPipelineMock() {
|
|
75
|
+
const checkpointStore = new Map();
|
|
76
|
+
setRunPipelineImplForTests(async (input, _deps, runtime) => {
|
|
77
|
+
if (input.confirmation?.job_confirmed !== true) {
|
|
78
|
+
return {
|
|
79
|
+
status: "NEED_CONFIRMATION",
|
|
80
|
+
required_confirmations: ["job"],
|
|
81
|
+
pending_questions: [{ field: "job", question: "请确认岗位" }]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (input.confirmation?.final_confirmed !== true) {
|
|
85
|
+
return {
|
|
86
|
+
status: "NEED_CONFIRMATION",
|
|
87
|
+
required_confirmations: ["final_review"],
|
|
88
|
+
pending_questions: [{ field: "final_review", question: "请最终确认" }]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
runtime?.onStage?.({ stage: "preflight", message: "preflight started" });
|
|
93
|
+
await sleep(30);
|
|
94
|
+
runtime?.onStage?.({ stage: "screen", message: "screen running" });
|
|
95
|
+
|
|
96
|
+
const checkpointPath = String(input.resume?.checkpoint_path || "mem://default");
|
|
97
|
+
const outputCsv = String(input.resume?.output_csv || "C:/tmp/mock.csv");
|
|
98
|
+
const total = 12;
|
|
99
|
+
let processed = input.resume?.resume === true ? Number(checkpointStore.get(checkpointPath) || 0) : 0;
|
|
100
|
+
if (!Number.isInteger(processed) || processed < 0) processed = 0;
|
|
101
|
+
|
|
102
|
+
while (processed < total) {
|
|
103
|
+
processed += 1;
|
|
104
|
+
runtime?.onProgress?.({
|
|
105
|
+
stage: "screen",
|
|
106
|
+
processed,
|
|
107
|
+
passed: Math.floor(processed / 3),
|
|
108
|
+
skipped: processed - Math.floor(processed / 3),
|
|
109
|
+
greet_count: 0,
|
|
110
|
+
line: `处理第 ${processed} 位候选人`
|
|
111
|
+
});
|
|
112
|
+
await sleep(25);
|
|
113
|
+
if (runtime?.isPauseRequested?.() === true) {
|
|
114
|
+
checkpointStore.set(checkpointPath, processed);
|
|
115
|
+
return {
|
|
116
|
+
status: "PAUSED",
|
|
117
|
+
result: {
|
|
118
|
+
processed_count: processed,
|
|
119
|
+
passed_count: Math.floor(processed / 3),
|
|
120
|
+
skipped_count: processed - Math.floor(processed / 3),
|
|
121
|
+
greet_count: 0,
|
|
122
|
+
output_csv: outputCsv,
|
|
123
|
+
checkpoint_path: checkpointPath,
|
|
124
|
+
completion_reason: "paused"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
checkpointStore.delete(checkpointPath);
|
|
131
|
+
return {
|
|
132
|
+
status: "COMPLETED",
|
|
133
|
+
result: {
|
|
134
|
+
processed_count: total,
|
|
135
|
+
passed_count: Math.floor(total / 3),
|
|
136
|
+
skipped_count: total - Math.floor(total / 3),
|
|
137
|
+
greet_count: 0,
|
|
138
|
+
output_csv: outputCsv,
|
|
139
|
+
completion_reason: "screen_completed"
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseDetachedSpawnArgs(argv = []) {
|
|
146
|
+
const normalized = Array.isArray(argv) ? argv.map((item) => String(item || "")) : [];
|
|
147
|
+
const runIdFlagIndex = normalized.indexOf("--run-id");
|
|
148
|
+
return {
|
|
149
|
+
runId: runIdFlagIndex >= 0 ? String(normalized[runIdFlagIndex + 1] || "").trim() : "",
|
|
150
|
+
resumeRun: normalized.includes("--resume")
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setupDetachedWorkerStub() {
|
|
155
|
+
setSpawnProcessImplForTests((command, argv = []) => {
|
|
156
|
+
assert.equal(typeof command, "string");
|
|
157
|
+
const { runId, resumeRun } = parseDetachedSpawnArgs(argv);
|
|
158
|
+
assert.equal(Boolean(runId), true, "detached worker spawn must include --run-id");
|
|
159
|
+
const pid = process.pid;
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
runDetachedWorkerForTests({
|
|
162
|
+
runId,
|
|
163
|
+
resumeRun,
|
|
164
|
+
workerPid: pid
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
}, 0);
|
|
167
|
+
return {
|
|
168
|
+
pid,
|
|
169
|
+
unref() {}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function testPauseAndResumeFlow() {
|
|
175
|
+
const runId = await startAcceptedRun("run for pause and resume", 11);
|
|
176
|
+
await waitForRunState(runId, ["running"]);
|
|
177
|
+
|
|
178
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 12);
|
|
179
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
180
|
+
|
|
181
|
+
const pausedRun = await waitForRunState(runId, ["paused"]);
|
|
182
|
+
assert.equal(pausedRun.state, "paused");
|
|
183
|
+
assert.equal(pausedRun.result?.status, "PAUSED");
|
|
184
|
+
assert.equal(Boolean(pausedRun.resume?.output_csv), true);
|
|
185
|
+
|
|
186
|
+
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 13);
|
|
187
|
+
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
188
|
+
assert.equal(resumePayload.run.run_id, runId);
|
|
189
|
+
|
|
190
|
+
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
191
|
+
assert.equal(completedRun.state, "completed");
|
|
192
|
+
assert.equal(completedRun.result?.status, "COMPLETED");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function testResumeAfterProcessRestartSimulation() {
|
|
196
|
+
const runId = await startAcceptedRun("run for restart resume", 21);
|
|
197
|
+
await waitForRunState(runId, ["running"]);
|
|
198
|
+
|
|
199
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 22);
|
|
200
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
201
|
+
await waitForRunState(runId, ["paused"]);
|
|
202
|
+
|
|
203
|
+
const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 23);
|
|
204
|
+
assert.equal(resumePayload.status, "RESUME_REQUESTED");
|
|
205
|
+
assert.equal(resumePayload.run.run_id, runId);
|
|
206
|
+
|
|
207
|
+
const completedRun = await waitForRunState(runId, ["completed"]);
|
|
208
|
+
assert.equal(completedRun.state, "completed");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function testCancelPausedRun() {
|
|
212
|
+
const runId = await startAcceptedRun("run for cancel paused", 31);
|
|
213
|
+
await waitForRunState(runId, ["running"]);
|
|
214
|
+
|
|
215
|
+
const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 32);
|
|
216
|
+
assert.equal(pausePayload.status, "PAUSE_REQUESTED");
|
|
217
|
+
await waitForRunState(runId, ["paused"]);
|
|
218
|
+
|
|
219
|
+
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 33);
|
|
220
|
+
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
221
|
+
|
|
222
|
+
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
223
|
+
assert.equal(canceledRun.state, "canceled");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function testCancelRunningRunKeepsCsv() {
|
|
227
|
+
const runId = await startAcceptedRun("run for cancel while running", 41);
|
|
228
|
+
await waitForRunState(runId, ["running"]);
|
|
229
|
+
|
|
230
|
+
const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 42);
|
|
231
|
+
assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
|
|
232
|
+
|
|
233
|
+
const canceledRun = await waitForRunState(runId, ["canceled"]);
|
|
234
|
+
assert.equal(canceledRun.state, "canceled");
|
|
235
|
+
assert.equal(canceledRun.error?.code, "PIPELINE_CANCELED");
|
|
236
|
+
assert.equal(Boolean(canceledRun.resume?.output_csv), true);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function main() {
|
|
240
|
+
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
241
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
242
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
243
|
+
setupPipelineMock();
|
|
244
|
+
setupDetachedWorkerStub();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await testPauseAndResumeFlow();
|
|
248
|
+
await testResumeAfterProcessRestartSimulation();
|
|
249
|
+
await testCancelPausedRun();
|
|
250
|
+
await testCancelRunningRunKeepsCsv();
|
|
251
|
+
console.log("index async tests passed");
|
|
252
|
+
} finally {
|
|
253
|
+
setRunPipelineImplForTests(null);
|
|
254
|
+
setSpawnProcessImplForTests(null);
|
|
255
|
+
if (previousHome === undefined) {
|
|
256
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
257
|
+
} else {
|
|
258
|
+
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
259
|
+
}
|
|
260
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await main();
|
package/src/test-pipeline.js
CHANGED
|
@@ -1505,6 +1505,72 @@ async function testSearchNoIframeWithLoginShouldReturnLoginRequired() {
|
|
|
1505
1505
|
assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
|
|
1506
1506
|
}
|
|
1507
1507
|
|
|
1508
|
+
async function testSearchNoIframeShouldRetryOnceWhenPageRecheckReady() {
|
|
1509
|
+
let searchCallCount = 0;
|
|
1510
|
+
let recheckCount = 0;
|
|
1511
|
+
const result = await runRecommendPipeline(
|
|
1512
|
+
{
|
|
1513
|
+
workspaceRoot: process.cwd(),
|
|
1514
|
+
instruction: "test",
|
|
1515
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
1516
|
+
overrides: {}
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
parseRecommendInstruction: () => createParsed(),
|
|
1520
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
1521
|
+
ensureBossRecommendPageReady: async () => {
|
|
1522
|
+
recheckCount += 1;
|
|
1523
|
+
return {
|
|
1524
|
+
ok: true,
|
|
1525
|
+
debug_port: 9222,
|
|
1526
|
+
state: "RECOMMEND_READY",
|
|
1527
|
+
page_state: {
|
|
1528
|
+
state: "RECOMMEND_READY",
|
|
1529
|
+
expected_url: "https://www.zhipin.com/web/chat/recommend",
|
|
1530
|
+
current_url: "https://www.zhipin.com/web/chat/recommend"
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
},
|
|
1534
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
1535
|
+
runRecommendSearchCli: async () => {
|
|
1536
|
+
searchCallCount += 1;
|
|
1537
|
+
if (searchCallCount === 1) {
|
|
1538
|
+
return {
|
|
1539
|
+
ok: false,
|
|
1540
|
+
stdout: "",
|
|
1541
|
+
stderr: "NO_RECOMMEND_IFRAME",
|
|
1542
|
+
structured: null,
|
|
1543
|
+
error: {
|
|
1544
|
+
code: "NO_RECOMMEND_IFRAME",
|
|
1545
|
+
message: "NO_RECOMMEND_IFRAME"
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
ok: true,
|
|
1551
|
+
summary: {
|
|
1552
|
+
candidate_count: 5,
|
|
1553
|
+
applied_filters: {}
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
},
|
|
1557
|
+
runRecommendScreenCli: async () => ({
|
|
1558
|
+
ok: true,
|
|
1559
|
+
summary: {
|
|
1560
|
+
processed_count: 2,
|
|
1561
|
+
passed_count: 1,
|
|
1562
|
+
skipped_count: 1,
|
|
1563
|
+
output_csv: "C:/temp/retry.csv"
|
|
1564
|
+
}
|
|
1565
|
+
})
|
|
1566
|
+
}
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
assert.equal(result.status, "COMPLETED");
|
|
1570
|
+
assert.equal(searchCallCount, 2);
|
|
1571
|
+
assert.equal(recheckCount >= 2, true);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1508
1574
|
async function testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin() {
|
|
1509
1575
|
const result = await runRecommendPipeline(
|
|
1510
1576
|
{
|
|
@@ -1940,6 +2006,7 @@ async function main() {
|
|
|
1940
2006
|
await testCompletedPipeline();
|
|
1941
2007
|
await testSearchFailure();
|
|
1942
2008
|
await testSearchNoIframeWithLoginShouldReturnLoginRequired();
|
|
2009
|
+
await testSearchNoIframeShouldRetryOnceWhenPageRecheckReady();
|
|
1943
2010
|
await testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin();
|
|
1944
2011
|
await testLoginRequiredShouldReturnGuidance();
|
|
1945
2012
|
await testDebugPortUnreachableShouldReturnConnectionCode();
|