fix(person): try connect deeplink fallback for follow_only profiles#467
fix(person): try connect deeplink fallback for follow_only profiles#467devag7 wants to merge 4 commits into
Conversation
Greptile SummaryThis PR adjusts LinkedIn connection detection to handle profiles where Connect may be hidden behind a deeplink fallback. It changes:
Confidence Score: 3/5This should be fixed before merging.
Important Files Changed
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
linkedin_mcp_server/scraping/extractor.py:134-136
**Keep action-root scoped**
`findActionRoot` now starts from every button in `main`, so the first unrelated button cluster with two interactive descendants can become the action root before the real profile action row is reached. In that case `_ACTION_SIGNALS_JS` reads compose, pending, and labeled-button signals from the wrong subtree, and `_OPEN_MORE_BUTTON_JS` can click the wrong expanded menu. A profile with a valid hidden Connect action can fail to open the real More menu, while a wrong root with an `aria-label` button and no compose anchor produces `state == "unavailable"` plus `has_labeled_action_button == true`, which reaches the fallback deeplink path without an invite anchor.
Reviews (4): Last reviewed commit: "fix(person): decouple action root detect..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates the LinkedIn connection flow to attempt the custom-invite deeplink even when the vanityName invite anchor is not present, and adds a regression test to verify the fallback behavior.
Changes:
- Add a deeplink “probe” fallback when
signals.has_invite_anchoris false. - Return
connect_unavailableonly if the deeplink does not open an invite dialog. - Add an async test covering the fallback navigation behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
linkedin_mcp_server/scraping/extractor.py |
Adds deeplink-probe fallback to connect_with_person when the invite anchor is missing. |
tests/test_scraping.py |
Adds test asserting the deeplink is navigated even when has_invite_anchor is false. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def mock_dialog(*args, **kwargs): | ||
| return True | ||
|
|
||
| with ( | ||
| patch.object( | ||
| extractor, | ||
| "scrape_person", | ||
| self._mock_scrape("Profile", follow_up_text="Profile"), | ||
| ), | ||
| patch.object( | ||
| extractor, | ||
| "_read_action_signals", | ||
| new_callable=AsyncMock, | ||
| side_effect=[self._signals(labeled_action=True), self._signals()], | ||
| ), | ||
| patch.object( | ||
| extractor, "_open_more_menu", new_callable=AsyncMock, return_value=True | ||
| ), | ||
| patch.object( | ||
| extractor, "_navigate_to_page", new_callable=AsyncMock | ||
| ) as mock_nav, | ||
| patch.object( | ||
| extractor, | ||
| "_dialog_is_open", | ||
| new_callable=AsyncMock, | ||
| side_effect=mock_dialog, | ||
| ), |
| await self._navigate_to_page(invite_url) | ||
| if not await self._dialog_is_open(timeout=5000): | ||
| return _connection_result( | ||
| url, | ||
| "connect_unavailable", | ||
| "LinkedIn did not expose a usable Connect action for this profile.", | ||
| profile=page_text, | ||
| ) | ||
| else: | ||
| await self._navigate_to_page(invite_url) | ||
|
|
| # Write-gate: Try to find the vanityName invite anchor. If missing, | ||
| # (e.g. because Connect is rendered as a <button> instead of an <a>, | ||
| # see Issue #454), fallback to probing the deeplink URL directly. |
| const candidates = main.querySelectorAll('button, a[href*="/messaging/compose/"]'); | ||
| for (const c of candidates) { | ||
| let el = c.parentElement; |
There was a problem hiding this comment.
findActionRoot now starts from every button in main, so the first unrelated button cluster with two interactive descendants can become the action root before the real profile action row is reached. In that case _ACTION_SIGNALS_JS reads compose, pending, and labeled-button signals from the wrong subtree, and _OPEN_MORE_BUTTON_JS can click the wrong expanded menu. A profile with a valid hidden Connect action can fail to open the real More menu, while a wrong root with an aria-label button and no compose anchor produces state == "unavailable" plus has_labeled_action_button == true, which reaches the fallback deeplink path without an invite anchor.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 134-136
Comment:
**Keep action-root scoped**
`findActionRoot` now starts from every button in `main`, so the first unrelated button cluster with two interactive descendants can become the action root before the real profile action row is reached. In that case `_ACTION_SIGNALS_JS` reads compose, pending, and labeled-button signals from the wrong subtree, and `_OPEN_MORE_BUTTON_JS` can click the wrong expanded menu. A profile with a valid hidden Connect action can fail to open the real More menu, while a wrong root with an `aria-label` button and no compose anchor produces `state == "unavailable"` plus `has_labeled_action_button == true`, which reaches the fallback deeplink path without an invite anchor.
**Context Used:** AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=7e01dc20-d2a2-4426-a426-ce1121bc577c))
How can I resolve this? If you propose a fix, please make it concise.
Fixes #454. Removes strict has_invite_anchor requirement and probes deeplink to detect Connect buttons hidden inside standard button elements.