# Phase 8.5: Phase 8 Fixes — chmod Removal, Mock Fixes, Cleanup

<!-- STATUS: COMPLETE -->

## Goal

Resolve all 27 remaining test failures from Phase 8 by removing chmod/config-file API key support, fixing LangChain mock setup, and cleaning up dead helpers. Zero failures before Phase 9.

## Dependencies

- **Phase 8** — Test files created, findings documented in `phases/8-tests-phases-6-7/findings.md` and `phase-fixes.md`
- **Decision** — chmod and config-file API key support removed (documented in `deferred.md`, `lessons.md`)

## Environment Constraints

- chmod blocked at kernel level — this phase removes all chmod from source, making it a non-issue going forward
- PYTHONPATH must be set manually: `PYTHONPATH=/project/src:/project/.venv/lib/python3.11/site-packages`

---

## Tasks

### Task 1: Remove chmod and config-file API key loading from source `DONE`

**Files:** `src/unfamiliar/config.py`, `src/unfamiliar/db/connection.py`, `src/unfamiliar/db/__init__.py`

**Action:**

`config.py`:
- Remove `import stat` (only used by `_has_strict_permissions`)
- Delete `_has_strict_permissions()` function entirely (lines 205-218)
- Line 86: remove `path.chmod(0o600)` — keep the `path.write_text()` and return
- Lines 101-103: remove the `_has_strict_permissions` check. Remove api_keys loading from config file entirely. Replace with `api_keys: dict[str, str] = {}` (always empty from file)
- In `_DEFAULT_CONFIG_CONTENT`: remove the commented `[api_keys]` section (lines 40-43) and the guidance about chmod 600
- `resolve_api_key()`: remove lines 134-136 (config file fallback). After env var checks, go straight to `raise APIKeyError`. Update docstring to say env-var-only resolution
- Keep `api_keys` field on `UnfamiliarConfig` with `default_factory=dict` — removing the field would cascade through callers. It just won't be populated from file anymore

`connection.py`:
- Remove `import os` if only used for `os.chmod`
- Remove lines 97-102 (the `db_path_str` / `os.chmod` block after schema init)

`db/__init__.py`:
- Remove `marker_path.chmod(0o600)` on line 116

**Verify:** `ruff check` passes on all three files. `python3 -c "from unfamiliar.config import load_config, resolve_api_key"` succeeds.

**Done when:** Zero chmod calls in `src/`. `grep -r "chmod" src/` returns nothing.

---

### Task 2: Update config tests `DONE`

**Files:** `tests/test_config.py`

**Action:**

Delete:
- `TestHasStrictPermissions` class entirely (4 tests) — function no longer exists
- `test_api_keys_skipped_without_strict_permissions` — concept no longer exists

Update `TestLoadConfig`:
- `test_loads_defaults_when_file_missing`: remove `assert config.api_keys == {}` if present (field still exists but is unrelated to file loading now). Should pass once source chmod removed.
- `test_loads_from_toml_file`: remove `config_path.chmod(0o600)`. Remove assertion about `config.api_keys`. Verify it loads `provider`, `token_budget`, `theme` from file.
- `test_creates_parent_directory`: remove any chmod-related assertions. Should pass once source chmod removed.

Update `TestResolveApiKey`:
- `test_config_key_used_if_no_env_vars`: this test expects config fallback to work — change it to expect `APIKeyError` since config fallback is removed
- `test_provider_env_var_takes_precedence`: remove `api_keys={"anthropic": "config-key"}` from UnfamiliarConfig construction (unnecessary noise now)
- `test_generic_env_var_used_if_provider_var_missing`: same cleanup

Add:
- `test_api_keys_section_in_config_file_is_ignored`: write a config file WITH `[api_keys]` section, load it, verify `config.api_keys` is empty dict

**Verify:** `pytest tests/test_config.py -v` — all pass. `ruff check tests/test_config.py` passes.

**Done when:** All config tests pass. No references to chmod or `_has_strict_permissions` in test file.

---

### Task 3: Fix mission generator mock setup `DONE`

**Files:** `tests/test_mission_generator.py`

**Action:**

The core problem: `mock_chat_model()` creates a MagicMock that doesn't work with LangChain's `|` pipe operator. `mock_mission_chain()` in conftest patches `MISSION_PROMPT.__or__` correctly.

Replace `mock_chat_model` usage with `mock_mission_chain` across all test classes. The import changes from:
```python
from tests.conftest import mock_chat_model
```
to:
```python
from tests.conftest import mock_mission_chain
```

**Per-class fix patterns:**

**Happy path / category / edge case / token budget tests** (tests that expect a successful response):
```python
# Before:
mock_model = mock_chat_model(response)
result = generate_category_missions(..., model=mock_model)

# After:
mock_model = MagicMock()
with mock_mission_chain(response):
    result = generate_category_missions(..., model=mock_model)
```

**Auth error test** (`test_auth_error_raises_immediately_not_wrapped`):
The auth error needs to come from the chain's invoke. Use `mock_mission_chain` but set side_effect on the yielded mock_chain:
```python
mock_model = MagicMock()
with mock_mission_chain(None) as mock_chain:
    mock_chain.invoke.side_effect = ExceptionWithStatusCode("Unauthorized", 401)
    with pytest.raises(AuthenticationError):
        generate_category_missions(..., model=mock_model)
```

**Transient error then success test:**
```python
call_count = 0
def invoke_side_effect(*args, **kwargs):
    nonlocal call_count
    call_count += 1
    if call_count == 1:
        raise ExceptionWithStatusCode("Service unavailable", 503)
    return response

mock_model = MagicMock()
with mock_mission_chain(None) as mock_chain:
    mock_chain.invoke.side_effect = invoke_side_effect
    result = generate_category_missions(..., model=mock_model)
```

**Permanent failure test:**
```python
mock_model = MagicMock()
with mock_mission_chain(None) as mock_chain:
    mock_chain.invoke.side_effect = ExceptionWithStatusCode("Service unavailable", 503)
    with pytest.raises(MissionGenerationError):
        generate_category_missions(..., model=mock_model)
```

**Empty response test:**
```python
response = make_category_response(missions=[])
mock_model = MagicMock()
with mock_mission_chain(response):
    with pytest.raises(MissionGenerationError):
        generate_category_missions(..., model=mock_model)
```

**`test_model_invoked_with_correct_input_data`:**
This test verifies call args on the mock. With mock_mission_chain, verify via `mock_chain.invoke.call_args` instead of `mock_structured.invoke.call_args`.

**`test_previous_missions_parameter_passed_correctly`:**
Same pattern — verify `build_mission_context` call args (already patched), and verify input_data via `mock_chain.invoke.call_args`.

**Note:** Tests that patch `time.sleep` at `unfamiliar.llm.missions.generator.time.sleep` — this module doesn't import `time` directly. The retry sleep is in `retry.py`. Patch `unfamiliar.llm.retry.time.sleep` instead, or remove the time.sleep patch if the test doesn't need it (mock_chain.invoke returns immediately).

**Verify:** `pytest tests/test_mission_generator.py -v` — all 19 tests pass. `ruff check tests/test_mission_generator.py` passes.

**Done when:** All mission generator tests pass using `mock_mission_chain`.

---

### Task 4: Clean up conftest helpers `DONE`

**Files:** `tests/conftest.py`, `tests/test_conftest_helpers.py`

**Action:**

`conftest.py`:
- Delete `mock_chat_model()` function — no longer used after Task 3
- Delete `mock_briefing_chain()` function — never imported anywhere (`test_llm_briefing.py` has its own local `_mock_briefing_chain`)
- Keep `mock_mission_chain()` — used by Task 3 fixes
- Update `sample_config()`: remove `api_keys={"anthropic": "sk-test-dummy-key-12345"}` parameter. Config no longer carries API keys.

`test_conftest_helpers.py`:
- Delete `TestMockChatModel` class (4 tests) — helper no longer exists
- Update `TestSampleConfig::test_has_api_key`: either delete this test (api_keys no longer populated) or change to `assert result.api_keys == {}` and rename to `test_api_keys_empty`

**Verify:** `pytest tests/test_co*_helpers.py -v` — all remaining tests pass. `ruff check` passes on both files.

**Done when:** No dead helpers in conftest. `sample_config()` produces config without api_keys.

---

### Task 5: Include validation.py fix `DONE`

**Files:** `src/unfamiliar/llm/missions/validation.py`

**Action:** The `line_end=0` fix (lines 177-182) is already in the working tree from the Phase 8 review session. No code change needed — just verify it's included when this phase is committed.

Verify the fix is correct: when `line_end` is 0 (or negative), it should be set to 1 with a warning, matching the existing `line_start` fix pattern.

**Verify:** `pytest tests/test_mission_validation_filerefs.py -v` — all pass including property-based tests.

**Done when:** Fix confirmed present and tests pass.

---

### Task 6: Clear Hypothesis database `DONE`

**Files:** `.hypothesis/` directory

**Action:** Delete the `.hypothesis/` directory to clear stale cached examples that may cause flaky test behavior. The property test `test_line_clamping_invariant` failed in one full-suite run but passed in isolation — stale examples from pre-fix validation code are the likely cause.

```bash
rm -rf .hypothesis/
```

**Verify:** Run `pytest tests/test_mission_validation_filerefs.py -v` twice — passes both times.

**Done when:** No `.hypothesis/` directory. Property tests stable.

---

### Task 7: Full suite verification and artifact update `DONE`

**Files:** `_planning/state.md`, `_planning/roadmap.md`

**Action:**

1. Run full test suite: `PYTHONPATH=/project/src:/project/.venv/lib/python3.11/site-packages pytest --tb=short -q`
2. Verify 0 failures
3. Run `ruff check src/ tests/` — verify 0 new errors
4. Update `state.md` with Phase 8.5 session log entry
5. Update `roadmap.md` to add Phase 8.5 between Phase 8 and Phase 9

**Verify:** Full suite green. Ruff clean. State and roadmap updated.

**Done when:** 0 failures, 0 ruff errors, planning docs current. Phase 8 fully closed. Ready for Phase 9 planning.

---

## Security Checklist

- No API keys stored in config files after this phase
- No chmod calls in source after this phase
- `resolve_api_key` uses environment variables only — no file-based secrets
- Secret scanning (`analysis/security/secrets.py`) unchanged — still reports locations without capturing values
- Test dummy keys removed from `sample_config()`

## Issues Discovered During Verification Stage

> (To be populated during build)
