## Implementation Plan ### Problem `_render_with_playwright` and the `crawl` top-level function in the DeepSearch crawler use an incorrect Playwright context manager pattern: they call `__aenter__` on the `async_playwright()` result but then attempt `__aexit__` on the **Playwright object** (the result of `__aenter__`) instead of on the **context manager** returned by `async_playwright()`. This causes an `AttributeError: 'Playwright' object has no attribute '__aexit__'` when Playwright is actually launched and then cleaned up. ### Files to Modify 1. **`devplacepy/services/jobs/deepsearch/crawl.py`** (or whatever file contains both functions — confirm absolute path via `git grep` before editing; assume `/workspace/repo/devplacepy/services/jobs/deepsearch/crawl.py` based on investigation line references). ### Changes #### 1. `_render_with_playwright` function – `own_browser=True` branch (around line 175) **Before:** ```python pw = await async_playwright().__aenter__() try: browser = await pw.chromium.launch(...) ... finally: await pw.__aexit__(None, None, None) ``` **After (use `async with`):** ```python async with async_playwright() as pw: browser = await pw.chromium.launch(...) ... ``` No manual `__aenter__`/`__aexit__` needed; the `async with` block handles it correctly. #### 2. `_render_with_playwright` function – shared browser case (already correct – `browser` passed in, no context manager needed) No change required. #### 3. `crawl` function (around line 294) – top-level browser launch and management **Before (conceptual):** ```python pw = await async_playwright().__aenter__() try: browser = await pw.chromium.launch(...) ... finally: await pw.__aexit__(None, None, None) ``` **After (use a separate variable for the manager):** ```python pw_manager = async_playwright() pw = await pw_manager.__aenter__() try: browser = await pw.chromium.launch(...) ... finally: await pw_manager.__aexit__(None, None, None) ``` This preserves the two-variable pattern needed because the browser lifecycle spans the whole function (cannot simply use `async with` since `browser` is passed to helper methods). (If the function already uses a `try/except` that catches launch failures, keep that; the fix only changes the `finally` cleanup target.) ### Verification Steps 1. **Run unit tests** `pip install -e '.[dev]' -q && make test-unit` All existing tests (including those that mock `fetch_page`) must pass. No new failures introduced. 2. **Run linter** `pip install -q ruff && ruff check .` Zero lint errors on the changed files (and ideally no new lint issues overall). 3. **Manual sanity check** (optional but recommended) - Confirm the fix compiles: `python -c "import ast; ast.parse(open('devplacepy/services/jobs/deepsearch/crawl.py').read())"` - If possible, run a deepsearch job that triggers the Playwright fallback to verify no `__aexit__` error (not strictly required for “Definition of Done” but good to confirm). ### Definition of Done - [ ] The two changes above are applied to the exact file(s) identified by `git grep`. - [ ] `make test-unit` runs to completion with zero failures (pre-existing failures in `test_compounds.py` and `test_owns_instance_true_for_creator` are unrelated and expected to remain). - [ ] `ruff check .` reports no errors on the modified file(s). - [ ] No new warnings or errors during the test run.