@reconcrap/boss-recommend-mcp 1.2.6 → 1.2.8
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/README.md +7 -0
- package/package.json +2 -1
- package/src/adapters.js +112 -60
- package/src/index.js +97 -0
- package/src/parser.js +5 -5
- package/src/pipeline.js +51 -1
- package/src/recommend-healing-config.js +131 -0
- package/src/recommend-healing-rules.json +261 -0
- package/src/self-heal.js +2237 -0
- package/src/test-pipeline.js +70 -10
- package/src/test-self-heal.js +224 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +570 -189
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +218 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +98 -50
package/src/test-pipeline.js
CHANGED
|
@@ -1432,6 +1432,7 @@ async function testCompletedPipeline() {
|
|
|
1432
1432
|
}
|
|
1433
1433
|
|
|
1434
1434
|
async function testSearchFailure() {
|
|
1435
|
+
let searchCallCount = 0;
|
|
1435
1436
|
const result = await runRecommendPipeline(
|
|
1436
1437
|
{
|
|
1437
1438
|
workspaceRoot: process.cwd(),
|
|
@@ -1444,22 +1445,80 @@ async function testSearchFailure() {
|
|
|
1444
1445
|
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
1445
1446
|
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
1446
1447
|
listRecommendJobs: async () => createJobListResult(),
|
|
1447
|
-
runRecommendSearchCli: async () =>
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1448
|
+
runRecommendSearchCli: async () => {
|
|
1449
|
+
searchCallCount += 1;
|
|
1450
|
+
return {
|
|
1451
|
+
ok: false,
|
|
1452
|
+
stdout: "",
|
|
1453
|
+
stderr: "boom",
|
|
1454
|
+
structured: null,
|
|
1455
|
+
error: {
|
|
1456
|
+
code: "RECOMMEND_FILTER_PANEL_UNAVAILABLE",
|
|
1457
|
+
message: "筛选面板不可用。"
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
},
|
|
1457
1461
|
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
1458
1462
|
}
|
|
1459
1463
|
);
|
|
1460
1464
|
|
|
1461
1465
|
assert.equal(result.status, "FAILED");
|
|
1462
1466
|
assert.equal(result.error.code, "RECOMMEND_FILTER_PANEL_UNAVAILABLE");
|
|
1467
|
+
assert.equal(searchCallCount, 3);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async function testSearchFilterFailureShouldRetryAndRecover() {
|
|
1471
|
+
let searchCallCount = 0;
|
|
1472
|
+
const result = await runRecommendPipeline(
|
|
1473
|
+
{
|
|
1474
|
+
workspaceRoot: process.cwd(),
|
|
1475
|
+
instruction: "test",
|
|
1476
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
1477
|
+
overrides: {}
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
parseRecommendInstruction: () => createParsed(),
|
|
1481
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
1482
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
1483
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
1484
|
+
runRecommendSearchCli: async () => {
|
|
1485
|
+
searchCallCount += 1;
|
|
1486
|
+
if (searchCallCount === 1) {
|
|
1487
|
+
return {
|
|
1488
|
+
ok: false,
|
|
1489
|
+
stdout: "",
|
|
1490
|
+
stderr: "FILTER_CONFIRM_FAILED",
|
|
1491
|
+
structured: null,
|
|
1492
|
+
error: {
|
|
1493
|
+
code: "FILTER_CONFIRM_FAILED",
|
|
1494
|
+
message: "FILTER_CONFIRM_FAILED"
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
ok: true,
|
|
1500
|
+
summary: {
|
|
1501
|
+
candidate_count: 6,
|
|
1502
|
+
applied_filters: {}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
},
|
|
1506
|
+
runRecommendScreenCli: async () => ({
|
|
1507
|
+
ok: true,
|
|
1508
|
+
summary: {
|
|
1509
|
+
processed_count: 3,
|
|
1510
|
+
passed_count: 2,
|
|
1511
|
+
skipped_count: 1,
|
|
1512
|
+
output_csv: "C:/temp/search-filter-retry.csv"
|
|
1513
|
+
}
|
|
1514
|
+
})
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
assert.equal(result.status, "COMPLETED");
|
|
1519
|
+
assert.equal(searchCallCount, 2);
|
|
1520
|
+
assert.equal(result.result.auto_recovery.trigger, "SEARCH_FILTER_RETRY");
|
|
1521
|
+
assert.equal(result.result.auto_recovery.attempt, 1);
|
|
1463
1522
|
}
|
|
1464
1523
|
|
|
1465
1524
|
async function testSearchNoIframeWithLoginShouldReturnLoginRequired() {
|
|
@@ -2005,6 +2064,7 @@ async function main() {
|
|
|
2005
2064
|
await testNeedFinalReviewConfirmationGate();
|
|
2006
2065
|
await testCompletedPipeline();
|
|
2007
2066
|
await testSearchFailure();
|
|
2067
|
+
await testSearchFilterFailureShouldRetryAndRecover();
|
|
2008
2068
|
await testSearchNoIframeWithLoginShouldReturnLoginRequired();
|
|
2009
2069
|
await testSearchNoIframeShouldRetryOnceWhenPageRecheckReady();
|
|
2010
2070
|
await testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin();
|
|
@@ -0,0 +1,224 @@
|
|
|
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 as indexTestables } from "./index.js";
|
|
6
|
+
import { runRecommendSelfHeal, __testables as selfHealTestables } from "./self-heal.js";
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
handleRequest,
|
|
10
|
+
setRunSelfHealImplForTests
|
|
11
|
+
} = indexTestables;
|
|
12
|
+
|
|
13
|
+
const TOOL_RUN_RECOMMEND_SELF_HEAL = "run_recommend_self_heal";
|
|
14
|
+
|
|
15
|
+
function makeToolCall(id, name, args = {}) {
|
|
16
|
+
return {
|
|
17
|
+
jsonrpc: "2.0",
|
|
18
|
+
id,
|
|
19
|
+
method: "tools/call",
|
|
20
|
+
params: {
|
|
21
|
+
name,
|
|
22
|
+
arguments: args
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readToolPayload(response) {
|
|
28
|
+
return response?.result?.structuredContent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callTool(name, args, id = 1) {
|
|
32
|
+
const response = await handleRequest(makeToolCall(id, name, args), process.cwd());
|
|
33
|
+
return {
|
|
34
|
+
payload: await readToolPayload(response),
|
|
35
|
+
response
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function testToolsListShouldIncludeSelfHeal() {
|
|
40
|
+
const response = await handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }, process.cwd());
|
|
41
|
+
const tools = response?.result?.tools || [];
|
|
42
|
+
assert.equal(tools.some((tool) => tool?.name === TOOL_RUN_RECOMMEND_SELF_HEAL), true);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function testIndexShouldRouteSelfHealTool() {
|
|
46
|
+
setRunSelfHealImplForTests(async () => ({ status: "HEALTHY", message: "ok" }));
|
|
47
|
+
try {
|
|
48
|
+
const { payload } = await callTool(TOOL_RUN_RECOMMEND_SELF_HEAL, {}, 2);
|
|
49
|
+
assert.equal(payload?.status, "HEALTHY");
|
|
50
|
+
} finally {
|
|
51
|
+
setRunSelfHealImplForTests(null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function testScanShouldCreateRepairSession() {
|
|
56
|
+
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
57
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-self-heal-home-"));
|
|
58
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
59
|
+
try {
|
|
60
|
+
const result = await runRecommendSelfHeal(
|
|
61
|
+
{ workspaceRoot: process.cwd(), args: { mode: "scan" } },
|
|
62
|
+
{
|
|
63
|
+
scanRuntimeSurface: async () => ({
|
|
64
|
+
selector_checks: [
|
|
65
|
+
{
|
|
66
|
+
rule_id: "filter_trigger",
|
|
67
|
+
path: ["frame", "filter_trigger"],
|
|
68
|
+
root: "frame",
|
|
69
|
+
matches: [
|
|
70
|
+
{ selector: ".filter-label-wrap", index: 0, count: 0 },
|
|
71
|
+
{ selector: ".recommend-filter.op-filter", index: 1, count: 1 }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
network_checks: [],
|
|
76
|
+
side_effect_summary: { opened_candidate_detail: false }
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
assert.equal(result.status, "NEED_CONFIRMATION");
|
|
81
|
+
assert.equal(typeof result.repair_session_id, "string");
|
|
82
|
+
assert.equal(result.proposed_repairs.length, 1);
|
|
83
|
+
const sessionPath = path.join(selfHealTestables.getSelfHealSessionsDir(), `${result.repair_session_id}.json`);
|
|
84
|
+
assert.equal(fs.existsSync(sessionPath), true);
|
|
85
|
+
} finally {
|
|
86
|
+
if (previousHome === undefined) {
|
|
87
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
88
|
+
} else {
|
|
89
|
+
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
90
|
+
}
|
|
91
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function testOptionalSelectorMissShouldNotBecomeDrift() {
|
|
96
|
+
const drifts = selfHealTestables.analyzeSelectorChecks([
|
|
97
|
+
{
|
|
98
|
+
rule_id: "featured_cards",
|
|
99
|
+
path: ["frame", "featured_cards"],
|
|
100
|
+
root: "frame",
|
|
101
|
+
required: false,
|
|
102
|
+
report_on_no_match: false,
|
|
103
|
+
skipped: false,
|
|
104
|
+
matches: [
|
|
105
|
+
{ selector: "li.geek-info-card", index: 0, count: 0 }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
]);
|
|
109
|
+
assert.equal(drifts.length, 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function testApplyShouldRequireConfirmation() {
|
|
113
|
+
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
114
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-self-heal-apply-home-"));
|
|
115
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
116
|
+
try {
|
|
117
|
+
const scanResult = await runRecommendSelfHeal(
|
|
118
|
+
{ workspaceRoot: process.cwd(), args: { mode: "scan" } },
|
|
119
|
+
{
|
|
120
|
+
scanRuntimeSurface: async () => ({
|
|
121
|
+
selector_checks: [
|
|
122
|
+
{
|
|
123
|
+
rule_id: "filter_trigger",
|
|
124
|
+
path: ["frame", "filter_trigger"],
|
|
125
|
+
root: "frame",
|
|
126
|
+
matches: [
|
|
127
|
+
{ selector: ".filter-label-wrap", index: 0, count: 0 },
|
|
128
|
+
{ selector: ".recommend-filter.op-filter", index: 1, count: 1 }
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
network_checks: [],
|
|
133
|
+
side_effect_summary: null
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
const result = await runRecommendSelfHeal({
|
|
138
|
+
workspaceRoot: process.cwd(),
|
|
139
|
+
args: {
|
|
140
|
+
mode: "apply",
|
|
141
|
+
repair_session_id: scanResult.repair_session_id
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
assert.equal(result.status, "FAILED");
|
|
145
|
+
assert.equal(result.error?.code, "SELF_HEAL_CONFIRMATION_REQUIRED");
|
|
146
|
+
} finally {
|
|
147
|
+
if (previousHome === undefined) {
|
|
148
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
149
|
+
} else {
|
|
150
|
+
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
151
|
+
}
|
|
152
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function testApplyShouldUpdateRulesFile() {
|
|
157
|
+
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
158
|
+
const previousRulesPath = process.env.BOSS_RECOMMEND_HEALING_RULES_FILE;
|
|
159
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-self-heal-rules-home-"));
|
|
160
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-self-heal-rules-"));
|
|
161
|
+
const rulesSourcePath = path.join(process.cwd(), "src", "recommend-healing-rules.json");
|
|
162
|
+
const tempRulesPath = path.join(tempDir, "recommend-healing-rules.json");
|
|
163
|
+
fs.copyFileSync(rulesSourcePath, tempRulesPath);
|
|
164
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
165
|
+
process.env.BOSS_RECOMMEND_HEALING_RULES_FILE = tempRulesPath;
|
|
166
|
+
try {
|
|
167
|
+
const scanResult = await runRecommendSelfHeal(
|
|
168
|
+
{ workspaceRoot: process.cwd(), args: { mode: "scan" } },
|
|
169
|
+
{
|
|
170
|
+
scanRuntimeSurface: async () => ({
|
|
171
|
+
selector_checks: [
|
|
172
|
+
{
|
|
173
|
+
rule_id: "filter_trigger",
|
|
174
|
+
path: ["frame", "filter_trigger"],
|
|
175
|
+
root: "frame",
|
|
176
|
+
matches: [
|
|
177
|
+
{ selector: ".filter-label-wrap", index: 0, count: 0 },
|
|
178
|
+
{ selector: ".recommend-filter.op-filter", index: 1, count: 1 }
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
network_checks: [],
|
|
183
|
+
side_effect_summary: null
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
const result = await runRecommendSelfHeal({
|
|
188
|
+
workspaceRoot: process.cwd(),
|
|
189
|
+
args: {
|
|
190
|
+
mode: "apply",
|
|
191
|
+
repair_session_id: scanResult.repair_session_id,
|
|
192
|
+
confirm_apply: true
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
assert.equal(result.status, "REPAIRED");
|
|
196
|
+
const updatedRules = JSON.parse(fs.readFileSync(tempRulesPath, "utf8"));
|
|
197
|
+
assert.equal(updatedRules.selectors.frame.filter_trigger[0], ".recommend-filter.op-filter");
|
|
198
|
+
} finally {
|
|
199
|
+
if (previousHome === undefined) {
|
|
200
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
201
|
+
} else {
|
|
202
|
+
process.env.BOSS_RECOMMEND_HOME = previousHome;
|
|
203
|
+
}
|
|
204
|
+
if (previousRulesPath === undefined) {
|
|
205
|
+
delete process.env.BOSS_RECOMMEND_HEALING_RULES_FILE;
|
|
206
|
+
} else {
|
|
207
|
+
process.env.BOSS_RECOMMEND_HEALING_RULES_FILE = previousRulesPath;
|
|
208
|
+
}
|
|
209
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
210
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function main() {
|
|
215
|
+
await testToolsListShouldIncludeSelfHeal();
|
|
216
|
+
await testIndexShouldRouteSelfHealTool();
|
|
217
|
+
await testScanShouldCreateRepairSession();
|
|
218
|
+
await testOptionalSelectorMissShouldNotBecomeDrift();
|
|
219
|
+
await testApplyShouldRequireConfirmation();
|
|
220
|
+
await testApplyShouldUpdateRulesFile();
|
|
221
|
+
console.log("self-heal tests passed");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await main();
|