@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.
@@ -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
- ok: false,
1449
- stdout: "",
1450
- stderr: "boom",
1451
- structured: null,
1452
- error: {
1453
- code: "RECOMMEND_FILTER_PANEL_UNAVAILABLE",
1454
- message: "筛选面板不可用。"
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();