@reconcrap/boss-recommend-mcp 1.2.5 → 1.2.7

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.
@@ -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();
@@ -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();