Skip to content

fix(applications): address code review issues#429

Open
benzntech wants to merge 3 commits into
stickerdaniel:mainfrom
benzntech:pr-425
Open

fix(applications): address code review issues#429
benzntech wants to merge 3 commits into
stickerdaniel:mainfrom
benzntech:pr-425

Conversation

@benzntech
Copy link
Copy Markdown

Summary

Address code review issues from PR #425:

  • Issue 1: Change destructiveHint to True for save_job tool
  • Issue 2: Add per-locale table for Easy Apply/Applied labels per CLAUDE.md scraping rules
  • Issue 3: Wait for confirmation after submit (dialog close or Applied badge)
  • Issue 4: Remove b.type==='submit' fallback (risky for multi-step flows)

Test plan

  • ruff check passes
  • ruff format passes
  • Live LinkedIn verification

nfsarch33 and others added 2 commits May 5, 2026 13:53
…asy_apply_submit

Adds job-application FastMCP tools to the personal nfsarch33 fork:

- save_job: bookmark a posting on LinkedIn (write, confirm_send-gated).
- list_applied: read /my-items/saved-jobs/?cardType=APPLIED for the
  user's applied-jobs history with extracted job IDs to reconcile
  against the cashflow ledger.
- easy_apply_inspect: open the Easy Apply dialog read-only and report
  step_count + question schema without submitting.
- easy_apply_submit: submit only zero-question, single-step Easy Apply
  postings; multi-step or question-bearing postings return
  status=manual_review with the detected questions for human follow-up.

All write actions follow the send_message pattern: confirm_send=False
returns a structured preview, confirm_send=True performs the action.
Detection uses structural DOM signals (aria-pressed, role=dialog,
aria-label presence) rather than locale-dependent verb text, per the
project's locale-independence rule.

Includes unit tests for the four new tools (preview, confirm, manual
review, list_applied) and adds them to the global timeout coverage.
Sentrux baseline saved at .sentrux/baseline.json (Quality 6104,
coupling 0.51, 0 cycles, 0 god files).
- Issue 1: Change destructiveHint to True for save_job
- Issue 2: Add per-locale table for Easy Apply/Applied labels
- Issue 3: Wait for confirmation after submit (dialog close or badge)
- Issue 4: Remove b.type==='submit' fallback (risky for multi-step)
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR addresses four code-review issues from PR #425: sets destructiveHint=True on save_job, introduces a _LOCALE_JOB_LABELS table for Easy Apply / Applied text detection, adds a post-submit confirmation wait, and removes the risky b.type==='submit' fallback from the click logic.

  • New MCP tools: save_job, list_applied, easy_apply_inspect, and easy_apply_submit are registered in a new applications.py module following the existing send_message pattern with confirm_send gating.
  • Confirmation wait is still a no-op: The page.evaluate() JavaScript loop exits immediately on every Chromium version because window.Promise is always truthy — the fix claimed in Issue 3 does not actually wait asynchronously. wait_for_function is needed instead (flagged in a prior review comment that remains unaddressed).
  • Dead code: easy_apply_full and its five supporting private methods (~285 lines, including a silently-broken resume-upload helper) are implemented but never registered as a tool and have no test coverage.

Confidence Score: 3/5

The submit confirmation wait introduced by this PR never actually waits — it snapshots the DOM synchronously and almost always reports submission_unconfirmed on real LinkedIn pages.

The JavaScript polling loop in easy_apply_submit exits on the first iteration in every modern browser because window.Promise is always defined, meaning the tool will report submission_unconfirmed for most real submissions even when the apply succeeded. Separately, _upload_resume_to_form returns true without actually attaching any file, silently dropping resumes when easy_apply_full is eventually wired up.

linkedin_mcp_server/scraping/extractor.py — the confirmation-wait JavaScript in easy_apply_submit and easy_apply_full, and the _upload_resume_to_form implementation.

Important Files Changed

Filename Overview
linkedin_mcp_server/scraping/extractor.py Adds job-application helpers (save_job, list_applied, easy_apply_*). The confirmation-wait JavaScript is a known no-op; _upload_resume_to_form silently fails to attach a file; easy_apply_full and ~285 lines of supporting helpers are dead code with no tool registration.
linkedin_mcp_server/tools/applications.py New file registering save_job, list_applied, easy_apply_inspect, and easy_apply_submit tools. Follows existing send_message patterns correctly with destructiveHint and readOnlyHint annotations; confirm_send gating is correct.
linkedin_mcp_server/server.py Adds import and registration call for register_application_tools; minimal, correct change.
tests/test_tools.py Adds TestApplicationTools covering save_job preview/confirm, list_applied, easy_apply_inspect, and easy_apply_submit paths; extends _TOOL_NAMES_FULL for timeout tests. No easy_apply_full coverage (mirrors the missing tool registration).
.sentrux/baseline.json New static quality-metrics baseline file; no logic impact.

Sequence Diagram

sequenceDiagram
    participant Client
    participant applications.py
    participant extractor.py
    participant LinkedIn

    Client->>applications.py: easy_apply_submit(job_id, confirm_send=True)
    applications.py->>extractor.py: easy_apply_submit(job_id, confirm_send=True)
    extractor.py->>extractor.py: _open_easy_apply_dialog(job_id)
    extractor.py->>LinkedIn: navigate to /jobs/view/{job_id}/
    LinkedIn-->>extractor.py: page loaded
    extractor.py->>LinkedIn: page.evaluate() detect Easy Apply button
    LinkedIn-->>extractor.py: {has_easy_apply, already_applied}
    extractor.py->>LinkedIn: page.evaluate() click Easy Apply
    LinkedIn-->>extractor.py: dialog opened
    extractor.py->>extractor.py: _inspect_easy_apply_dialog()
    extractor.py->>LinkedIn: page.evaluate() read dialog structure
    LinkedIn-->>extractor.py: {step_count, questions, submit_button}
    extractor.py->>extractor.py: _click_easy_apply_submit()
    extractor.py->>LinkedIn: page.evaluate() click Submit
    LinkedIn-->>extractor.py: clicked=true
    Note over extractor.py,LinkedIn: JS loop exits immediately, window.Promise always truthy
    extractor.py->>LinkedIn: page.evaluate() check dialog/badge no-op
    LinkedIn-->>extractor.py: DOM snapshot, no real wait
    extractor.py->>extractor.py: _dismiss_dialog()
    extractor.py-->>applications.py: {status: submitted or submission_unconfirmed}
    applications.py-->>Client: result dict
Loading

Comments Outside Diff (1)

  1. linkedin_mcp_server/scraping/extractor.py, line 619-650 (link)

    P1 _upload_resume_to_form silently reports success without uploading

    page.evaluate() runs in a sandboxed JavaScript context where setting file input values is blocked by the browser for security reasons. The comment inside the function acknowledges this ("Can't actually set file content"), yet the function still returns true and easy_apply_full logs "Resume upload triggered" and waits 1 second as if a real upload occurred. When easy_apply_full is later wired up as an MCP tool, callers who pass resume_path will receive a submitted response while the actual application has no resume attached. Use await self._page.locator('input[type="file"]').set_input_files(resume_path) instead.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: linkedin_mcp_server/scraping/extractor.py
    Line: 619-650
    
    Comment:
    **`_upload_resume_to_form` silently reports success without uploading**
    
    `page.evaluate()` runs in a sandboxed JavaScript context where setting file input values is blocked by the browser for security reasons. The comment inside the function acknowledges this ("Can't actually set file content"), yet the function still returns `true` and `easy_apply_full` logs "Resume upload triggered" and waits 1 second as if a real upload occurred. When `easy_apply_full` is later wired up as an MCP tool, callers who pass `resume_path` will receive a `submitted` response while the actual application has no resume attached. Use `await self._page.locator('input[type="file"]').set_input_files(resume_path)` instead.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
linkedin_mcp_server/scraping/extractor.py:619-650
**`_upload_resume_to_form` silently reports success without uploading**

`page.evaluate()` runs in a sandboxed JavaScript context where setting file input values is blocked by the browser for security reasons. The comment inside the function acknowledges this ("Can't actually set file content"), yet the function still returns `true` and `easy_apply_full` logs "Resume upload triggered" and waits 1 second as if a real upload occurred. When `easy_apply_full` is later wired up as an MCP tool, callers who pass `resume_path` will receive a `submitted` response while the actual application has no resume attached. Use `await self._page.locator('input[type="file"]').set_input_files(resume_path)` instead.

### Issue 2 of 2
linkedin_mcp_server/scraping/extractor.py:652-766
**`easy_apply_full` and its helpers are dead code — never registered as a tool**

`easy_apply_full`, `_get_next_button`, `_has_next_step`, `_auto_fill_fields`, and `_upload_resume_to_form` (~285 lines) are implemented here but `easy_apply_full` is never registered in `applications.py`. None of the tests in `TestApplicationTools` exercise this path. If this is intentional deferral, add a `# TODO` comment so reviewers know. If it was meant to be in this PR, a corresponding `@mcp.tool(...)` registration is missing.

Reviews (2): Last reviewed commit: "feat(applications): add multi-step Easy ..." | Re-trigger Greptile

Comment on lines +3191 to +3201
clicked = await self._page.evaluate(
"""() => {
const btn = Array.from(document.querySelectorAll(
'main button[aria-label*="Easy Apply"]'
)).find(b => !b.disabled
&& (b.offsetWidth || b.offsetHeight || b.getClientRects().length));
if (!btn) return false;
btn.click();
return true;
}"""
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Easy Apply click selector ignores locale — the detection step uses the locale-aware easy_apply_label from _get_job_labels(), but the subsequent click still hardcodes "Easy Apply". Adding any non-English locale to _LOCALE_JOB_LABELS would detect the button correctly but fail to click it. Use the variable consistently.

Suggested change
clicked = await self._page.evaluate(
"""() => {
const btn = Array.from(document.querySelectorAll(
'main button[aria-label*="Easy Apply"]'
)).find(b => !b.disabled
&& (b.offsetWidth || b.offsetHeight || b.getClientRects().length));
if (!btn) return false;
btn.click();
return true;
}"""
)
clicked = await self._page.evaluate(
f"""() => {{
const btn = Array.from(document.querySelectorAll(
'main button[aria-label*="{easy_apply_label}"]'
)).find(b => !b.disabled
&& (b.offsetWidth || b.offsetHeight || b.getClientRects().length));
if (!btn) return false;
btn.click();
return true;
}}"""
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 3191-3201

Comment:
**Easy Apply click selector ignores locale** — the detection step uses the locale-aware `easy_apply_label` from `_get_job_labels()`, but the subsequent click still hardcodes `"Easy Apply"`. Adding any non-English locale to `_LOCALE_JOB_LABELS` would detect the button correctly but fail to click it. Use the variable consistently.

```suggestion
        clicked = await self._page.evaluate(
            f"""() => {{
                const btn = Array.from(document.querySelectorAll(
                    'main button[aria-label*="{easy_apply_label}"]'
                )).find(b => !b.disabled
                    && (b.offsetWidth || b.offsetHeight || b.getClientRects().length));
                if (!btn) return false;
                btn.click();
                return true;
            }}"""
        )
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +307 to +314
def _get_job_labels() -> tuple[str, str]:
"""Return (easy_apply, applied) labels for the current locale.

Falls back to English if locale is not in the table.
"""
# Could detect locale from page, but LinkedIn UI locale is usually English.
labels = _LOCALE_JOB_LABELS.get("en", ("Easy Apply", "Applied"))
return labels
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _get_job_labels() is effectively hardcoded to English. .get("en", ...) always succeeds because "en" is unconditionally present in _LOCALE_JOB_LABELS, making the fallback argument dead code and concealing the fact that locale detection hasn't been wired up. A future caller adding a new locale will get English labels silently. Add a locale parameter and a TODO comment.

Suggested change
def _get_job_labels() -> tuple[str, str]:
"""Return (easy_apply, applied) labels for the current locale.
Falls back to English if locale is not in the table.
"""
# Could detect locale from page, but LinkedIn UI locale is usually English.
labels = _LOCALE_JOB_LABELS.get("en", ("Easy Apply", "Applied"))
return labels
def _get_job_labels(locale: str = "en") -> tuple[str, str]:
"""Return (easy_apply, applied) labels for the given locale.
Falls back to English if locale is not in the table.
"""
# TODO: detect locale from the page (e.g. <html lang="..."> or
# navigator.language) and pass it here instead of defaulting to "en".
return _LOCALE_JOB_LABELS.get(locale, _LOCALE_JOB_LABELS["en"])
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 307-314

Comment:
**`_get_job_labels()` is effectively hardcoded to English.** `.get("en", ...)` always succeeds because `"en"` is unconditionally present in `_LOCALE_JOB_LABELS`, making the fallback argument dead code and concealing the fact that locale detection hasn't been wired up. A future caller adding a new locale will get English labels silently. Add a `locale` parameter and a TODO comment.

```suggestion
def _get_job_labels(locale: str = "en") -> tuple[str, str]:
    """Return (easy_apply, applied) labels for the given locale.

    Falls back to English if locale is not in the table.
    """
    # TODO: detect locale from the page (e.g. <html lang="..."> or
    # navigator.language) and pass it here instead of defaulting to "en".
    return _LOCALE_JOB_LABELS.get(locale, _LOCALE_JOB_LABELS["en"])
```

How can I resolve this? If you propose a fix, please make it concise.

- Add _get_next_button() to click "Continue to next step"
- Add _has_next_step() to detect additional form steps
- Add _auto_fill_fields() for form auto-fill with answers dict
- Add _upload_resume_to_form() for resume upload handling
- Add easy_apply_full() orchestrator for full multi-step flow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants