Spaces:
Sleeping
Sleeping
Commit Β·
7499765
1
Parent(s): 7ea42ad
docs(phase-6): verification (5/5 must-haves pass; human/deploy items pending)
Browse filesParity test 5 passed; no ATS/LLM in extension JS; token not hardcoded; single
source of truth. Remaining items are deployment/runtime (live API end-to-end +
Streamlit-on-7860 coexistence).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.planning/phases/06-chrome-extension/06-VERIFICATION.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
phase: 06-chrome-extension
|
| 3 |
+
verified: 2026-06-20T00:00:00Z
|
| 4 |
+
status: human_needed
|
| 5 |
+
score: 5/5 must-haves verified
|
| 6 |
+
human_verification:
|
| 7 |
+
- test: "Load extension unpacked in Chrome (chrome://extensions β Load unpacked β extension/) and navigate to a LinkedIn job listing. Click extension icon β popup opens β job title and company appear within 1-2 seconds."
|
| 8 |
+
expected: "No manifest errors shown. Options page saves API URL, token, and resume PDF. Popup shows extracted job title and company. Run button triggers spinner."
|
| 9 |
+
why_human: "Chrome extension manifest loading, content-script DOM extraction, and chrome.storage.local round-trip cannot be exercised by automated Python tests."
|
| 10 |
+
- test: "With a locally running api_server.py (python api_server.py), open Options, set api_url=http://localhost:7860 and a matching API_SECRET_TOKEN token, upload a PDF resume, then click Run on a LinkedIn/Naukri/Indeed job page."
|
| 11 |
+
expected: "Spinner appears, status badge (READY/READY_REVIEW/etc.) and scores table are displayed. Download DOCX button triggers a file download."
|
| 12 |
+
why_human: "Full end-to-end flow (extension β local API β LLM pipeline β DOCX download) requires a live server. The HF Space deployment path additionally requires API keys and HF secrets."
|
| 13 |
+
- test: "Verify the Streamlit web UI still loads at http://localhost:7860 after starting python api_server.py."
|
| 14 |
+
expected: "Streamlit UI renders at the root URL, proxied through FastAPI. The extension API and Streamlit app coexist on port 7860."
|
| 15 |
+
why_human: "Subprocess launch of Streamlit on 8501 and httpx reverse-proxy to 7860 cannot be smoke-tested without a running server process."
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# Phase 6: Chrome Extension Verification Report
|
| 19 |
+
|
| 20 |
+
**Phase Goal:** Avoid IP-blocking by applying one job at a time in-browser. User opens a job listing, clicks Run in a Chrome extension; the extension extracts the JD and sends it with the pre-uploaded resume to the SAME existing Python pipeline (now also a JSON API on the HF Space) which tailors+scores+renders with IDENTICAL scoring and returns a DOCX/PDF download. The HF Space serves both the standalone Streamlit app and the extension API from one codebase.
|
| 21 |
+
**Verified:** 2026-06-20
|
| 22 |
+
**Status:** human_needed
|
| 23 |
+
**Re-verification:** No β initial verification
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## Goal Achievement
|
| 28 |
+
|
| 29 |
+
### Observable Truths
|
| 30 |
+
|
| 31 |
+
| # | Truth | Status | Evidence |
|
| 32 |
+
|---|-------|--------|----------|
|
| 33 |
+
| 1 | R-EXT1: No ATS/LLM/scoring logic in any extension JS | VERIFIED | grep of background.js, content.js, popup.js for ats_score/jd_match/keyword/llm/openai/generate_resume returns zero matches. jd_match appears in popup.js only as a display key rendering the API's returned score value (L172, L176), not computing it. |
|
| 34 |
+
| 2 | R-EXT2: api_server reuses existing pipeline; parity tests pass | VERIFIED | api_server.py calls `_run_provider_chain(base_resume_override=base_resume)` on L130. `pytest tests/test_api_parity.py tests/test_api_health.py -v` β 5 passed in 3.87s. test_api_parity_deterministic PASSED (data/resume/resume.pdf present locally). |
|
| 35 |
+
| 3 | R-EXT3: JD extraction with LinkedIn/Naukri/Indeed selectors + generic fallback | VERIFIED | content.js (281 lines) contains extractLinkedIn, extractNaukri, extractIndeed, extractGenericJD, MutationObserver waitForElement, platform router, EXTRACT_JD message listener, and 16000-char truncation. |
|
| 36 |
+
| 4 | R-EXT4: Resume stored in extension; token not hardcoded | VERIFIED | options.js writes resume_b64 + api_token to chrome.storage.local.set. background.js reads via chrome.storage.local.get(['resume_b64', 'api_url', 'api_token']). No hardcoded token string found in any JS file (checked with 32+ char literal regex). |
|
| 37 |
+
| 5 | R-EXT5: Download in-page; download buttons gated on download_allowed | VERIFIED | popup.js L188: `if (result.download_allowed) { downloadsEl.classList.add('visible') }`. background.js uses URL.createObjectURL + chrome.downloads.download. Downloads container is hidden by default and only revealed on download_allowed=true. |
|
| 38 |
+
|
| 39 |
+
**Score: 5/5 truths verified (automated)**
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Required Artifacts
|
| 44 |
+
|
| 45 |
+
| Artifact | Expected | Status | Details |
|
| 46 |
+
|----------|----------|--------|---------|
|
| 47 |
+
| `api_server.py` | FastAPI on port 7860 with /api/health, /api/generate, Streamlit reverse proxy | VERIFIED | 361 lines. Syntax OK. Imports from src.resume_parser_v2, src.resume_customizer, src.llm_client, src.providers. start_streamlit() guarded in `__main__` β safe to import in tests. |
|
| 48 |
+
| `requirements.txt` | fastapi, uvicorn[standard], websockets, python-multipart added | VERIFIED | All 4 packages present. |
|
| 49 |
+
| `start.sh` | Launches python api_server.py (not streamlit directly) | VERIFIED | L46: `echo "=== Starting FastAPI + Streamlit (via api_server.py) ==="` L48: `exec python api_server.py`. No `streamlit run ui.py` after the api_server reference. |
|
| 50 |
+
| `extension/manifest.json` | MV3, storage/downloads/activeTab/scripting permissions, options_ui, content_scripts | VERIFIED | manifest_version=3, all 4 permissions present, options_ui pointing to options/options.html, content_scripts with content.js at document_idle, background service_worker=background.js. |
|
| 51 |
+
| `extension/options/options.html` | Resume upload, API URL, API token fields; all required IDs | VERIFIED | All 6 required IDs present: api_url, api_token, resume_file, resume_status, save_btn, status_msg. |
|
| 52 |
+
| `extension/options/options.js` | PDF β base64 + SHA-256 β chrome.storage.local; token from form | VERIFIED | chrome.storage.local.set, resume_b64, sha256, api_token all present. Token read from `document.getElementById('api_token').value` β not hardcoded. |
|
| 53 |
+
| `extension/content.js` | LinkedIn/Naukri/Indeed selectors + generic fallback + EXTRACT_JD message listener | VERIFIED | 281 lines. All 10 required symbols present. extractLinkedIn is async with waitForElement MutationObserver. |
|
| 54 |
+
| `extension/popup/popup.html` | Run button, spinner, status, scores table, Download DOCX/PDF buttons | VERIFIED | All 8 required IDs present including downloads container and individual score cells. |
|
| 55 |
+
| `extension/popup/popup.js` | EXTRACT_JD β GENERATE β DOWNLOAD orchestration; download gating | VERIFIED | EXTRACT_JD, GENERATE, DOWNLOAD, download_allowed, docx_b64, chrome.tabs.query, chrome.runtime.sendMessage all present. |
|
| 56 |
+
| `extension/background.js` | GENERATE (API call) + DOWNLOAD (chrome.downloads); no ATS logic | VERIFIED | 99 lines. All required symbols present. No forbidden symbols found. Token read from storage, not hardcoded. |
|
| 57 |
+
| `tests/test_api_health.py` | 3 smoke tests; health, token gate, missing resume | VERIFIED | 3 tests PASSED. |
|
| 58 |
+
| `tests/test_api_parity.py` | Parity test via StubProvider; skips on absent resume.pdf | VERIFIED | test_fixture_jd_exists PASSED. test_api_parity_deterministic PASSED (resume.pdf present). |
|
| 59 |
+
| `tests/fixtures/sample_jd.txt` | PM JD fixture, 200+ chars | VERIFIED | 2342 chars, "Product Manager" present, TestCo company. |
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Key Link Verification
|
| 64 |
+
|
| 65 |
+
| From | To | Via | Status | Details |
|
| 66 |
+
|------|----|-----|--------|---------|
|
| 67 |
+
| api_server.generate_resume_for_api | src/resume_customizer._run_provider_chain | base_resume_override kwarg | WIRED | api_server.py L130: `customizer._run_provider_chain(job, filepath, provider_chain, base_resume_override=base_resume)` |
|
| 68 |
+
| api_server proxy route | Streamlit on localhost:8501 | httpx.AsyncClient + websockets | WIRED | HTTP catch-all proxy at L321-354. WebSocket proxy at L265-316. |
|
| 69 |
+
| start.sh | api_server.py | exec python api_server.py | WIRED | L48: `exec python api_server.py` |
|
| 70 |
+
| options.js | chrome.storage.local | chrome.storage.local.set({resume_b64, api_token, api_url}) | WIRED | options.js L103: `chrome.storage.local.set(toStore, ...)` with resume_b64, api_token, api_url in toStore |
|
| 71 |
+
| background.js | api_server /api/generate | fetch(api_url + '/api/generate', X-Api-Token header, FormData) | WIRED | background.js L48: `fetch(...'/api/generate', {headers: {'X-Api-Token': ...}, body: formData})` |
|
| 72 |
+
| background.js | chrome.downloads | URL.createObjectURL + chrome.downloads.download | WIRED | background.js L90-94 |
|
| 73 |
+
| popup.js | content.js | chrome.tabs.sendMessage(tabId, {type: 'EXTRACT_JD'}) | WIRED | popup.js sends EXTRACT_JD; content.js message listener responds |
|
| 74 |
+
| popup.js | background.js | chrome.runtime.sendMessage({type: 'GENERATE'}) | WIRED | popup.js sends GENERATE; background.js onMessage handles it |
|
| 75 |
+
| tests/test_api_parity.py | api_server.generate_resume_for_api | direct call with StubProvider monkey-patch | WIRED | L83: `api_server.generate_resume_for_api(pdf_bytes, jd_text, job_title, company)` |
|
| 76 |
+
| tests/test_api_health.py | api_server.app | TestClient(api_server.app) | WIRED | L28: `client = TestClient(api_server.app)` |
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## Requirements Coverage
|
| 81 |
+
|
| 82 |
+
| Requirement | Plans | Description | Status | Evidence |
|
| 83 |
+
|-------------|-------|-------------|--------|----------|
|
| 84 |
+
| R-EXT1 | 06-01, 06-04 | Thin-client extension: no ATS/LLM logic in JS | SATISFIED | grep of all 4 extension JS files returns zero forbidden symbols |
|
| 85 |
+
| R-EXT2 | 06-01, 06-05 | Same scoring as web app (parity test) | SATISFIED | test_api_parity_deterministic PASSED with StubProvider; api_server calls _run_provider_chain not a fork |
|
| 86 |
+
| R-EXT3 | 06-03 | Generic + curated JD extraction (LinkedIn/Naukri/Indeed) | SATISFIED | content.js has platform-specific selectors + generic text-density fallback + MutationObserver for SPA |
|
| 87 |
+
| R-EXT4 | 06-02, 06-04 | Resume stored locally in extension; token not hardcoded | SATISFIED | options.js stores resume_b64 + api_token to chrome.storage.local; background.js reads from storage |
|
| 88 |
+
| R-EXT5 | 06-03, 06-04 | Download DOCX/PDF in-page; gated on download_allowed | SATISFIED | popup.js gates downloads on download_allowed; background.js triggers chrome.downloads.download |
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## Anti-Patterns Found
|
| 93 |
+
|
| 94 |
+
| File | Line | Pattern | Severity | Impact |
|
| 95 |
+
|------|------|---------|----------|--------|
|
| 96 |
+
| None found | β | β | β | β |
|
| 97 |
+
|
| 98 |
+
No TODO/FIXME/placeholder comments, empty implementations, or return-null stubs found in any phase artifact. All functions have substantive implementations.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## Human Verification Required
|
| 103 |
+
|
| 104 |
+
### 1. Chrome Extension Load and JD Extraction
|
| 105 |
+
|
| 106 |
+
**Test:** Load the extension unpacked in Chrome (chrome://extensions β Developer mode β Load unpacked β select the `extension/` folder). Navigate to any LinkedIn, Naukri, or Indeed job listing. Click the extension icon.
|
| 107 |
+
**Expected:** No manifest errors shown in chrome://extensions. Options page opens correctly (right-click β Options). Popup shows extracted job title and company within 1-2 seconds of opening.
|
| 108 |
+
**Why human:** Chrome extension manifest loading, MV3 service worker registration, content-script injection at document_idle, and chrome.storage.local are Chrome-runtime APIs that cannot be exercised by Python tests.
|
| 109 |
+
|
| 110 |
+
### 2. Full End-to-End Run with Local API
|
| 111 |
+
|
| 112 |
+
**Test:** Run `python api_server.py` locally. Open Options, set api_url=`http://localhost:7860`, paste a token matching `API_SECRET_TOKEN` env var, upload a PDF resume. Navigate to a job page and click Run.
|
| 113 |
+
**Expected:** Spinner appears (15-60s for LLM). Status badge (READY/READY_REVIEW/etc.) and scores table appear. Clicking Download DOCX saves a file to disk.
|
| 114 |
+
**Why human:** Full pipeline run requires live LLM providers (Kimi/NVIDIA/GLM). The end-to-end extension β API β pipeline β download flow cannot be exercised without a running server.
|
| 115 |
+
|
| 116 |
+
### 3. Streamlit Reverse-Proxy Coexistence
|
| 117 |
+
|
| 118 |
+
**Test:** After `python api_server.py` starts, open `http://localhost:7860` in a browser.
|
| 119 |
+
**Expected:** The Streamlit web UI loads and is functional. The API endpoints (`/api/health`, `/api/generate`) also respond. Both work from the same port 7860.
|
| 120 |
+
**Why human:** Verifying the subprocess launch of Streamlit on 8501 and httpx proxy routing to 7860 requires a running process; the TestClient in health tests does not start Streamlit.
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## Gaps Summary
|
| 125 |
+
|
| 126 |
+
No automated gaps found. All 5 must-have truths are verified in the actual codebase. All 13 artifacts exist and are substantive (not stubs). All 10 key links are wired. All 5 requirements are satisfied by code evidence. Pytest exits 0 with 5/5 passed.
|
| 127 |
+
|
| 128 |
+
The three human-verification items are deployment/runtime checks that are explicitly out-of-scope for automated verification per the must_verify note: "full live end-to-end (real Kimi/NVIDIA + deployed HF API) is NOT runnable here β mark that as human/deployment-gated, not failed."
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
_Verified: 2026-06-20_
|
| 133 |
+
_Verifier: Claude (gsd-verifier)_
|