# Phase 8 Fixes

<!-- STATUS: DRAFT -->

Fixes required to resolve remaining test failures and architectural drift from Phase 8.

**Current state:** 28 tests failing (698 passing)

---

## Fix 1: Remove chmod and config-file API key support

**Decision:** Drop all `chmod` calls from the codebase and remove the ability to store API keys in the config file. API keys will be resolved exclusively from environment variables.

**Rationale:** This app is a codebase onboarding/gamification tool, not a secrets manager. Automatically changing permissions on a user's files is outside the app's scope. The config-file API key feature was the sole reason for chmod — removing it eliminates the need entirely.

**Tests resolved:** 8 (`test_config.py` — all `TestLoadConfig` and `TestHasStrictPermissions` failures)

### Source changes

**`src/unfamiliar/config.py`:**
- Remove `import stat` (no longer needed)
- Remove `_has_strict_permissions()` function entirely
- Remove `api_keys` field from `UnfamiliarConfig` (or keep as empty dict, never populated from file)
- Remove `path.chmod(0o600)` call on line 86 (new config file creation)
- Remove the `_has_strict_permissions` check on line 102 and the `api_keys` loading from config file
- Remove the `[api_keys]` commented section from `_DEFAULT_CONFIG_CONTENT`
- `resolve_api_key()`: remove the config file fallback (lines 134-136), keep env var resolution only
- Update docstrings to reflect env-var-only key resolution

**`src/unfamiliar/db/connection.py`:**
- Remove `import os` if only used for chmod
- Remove `os.chmod(db_path, 0o600)` on line 102 and the surrounding conditional block

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

### Test changes

**`tests/test_config.py`:**
- Delete `TestHasStrictPermissions` class entirely (4 tests) — function no longer exists
- Delete or rewrite `TestLoadConfig` tests that set chmod:
  - `test_loads_from_toml_file`: remove `config_path.chmod(0o600)` call, remove api_keys assertion
  - `test_api_keys_skipped_without_strict_permissions`: delete entirely — no longer applicable
  - `test_loads_defaults_when_file_missing`: remove any chmod assertion, should pass as-is after source change
  - `test_creates_parent_directory`: remove any chmod assertion, should pass as-is after source change
- Add new test: `test_api_keys_not_loaded_from_config_file` — verify that even if `[api_keys]` section exists in config file, it is ignored
- Update `test_resolve_api_key_*` tests to remove config-file fallback scenarios

---

## Fix 2: Fix LangChain mock setup in mission generator tests

**Tests resolved:** 19 (`test_mission_generator.py`)

**Root cause:** `mock_chat_model()` creates a `MagicMock` that doesn't work with LangChain's `|` (pipe) operator. The production code does `chain = MISSION_PROMPT | structured_model` — LangChain wraps the MagicMock in a `RunnableLambda`, bypassing the mock's `invoke()`. The chain returns a raw MagicMock instead of the configured response.

`conftest.py` already has `mock_mission_chain()` which patches `MISSION_PROMPT.__or__` to return a mock chain directly. This helper works correctly but none of the 19 failing tests use it.

### Fix approach

Update all tests in `test_mission_generator.py` to use `mock_mission_chain()` instead of `mock_chat_model()`. The pattern change:

**Before (broken):**
```python
mock_model = mock_chat_model(response)
result = generate_category_missions(..., model=mock_model)
```

**After (working):**
```python
mock_model = MagicMock()  # model still needed as argument
with mock_mission_chain(response) as mock_chain:
    result = generate_category_missions(..., model=mock_model)
```

### Affected tests

All 7 test classes in `test_mission_generator.py`:
- `TestGenerateCategoryMissionsHappyPath` (3 tests)
- `TestGenerateCategoryMissionsAllCategories` (7 parameterized tests)
- `TestGenerateCategoryMissionsAuthError` (1 test) — already uses MagicMock directly, but still fails due to pipe operator
- `TestGenerateCategoryMissionsTransientErrors` (1 test) — same issue
- `TestGenerateCategoryMissionsPermanentFailure` (2 tests) — same issue
- `TestGenerateCategoryMissionsEmptyResponse` (1 test)
- `TestGenerateCategoryMissionsEdgeCases` (2 tests)
- `TestGenerateCategoryMissionsTokenBudget` (2 tests)

### Additional consideration

Tests that verify `mock_model.with_structured_output.assert_called_once_with(...)` or `mock_structured.invoke.assert_called_once()` will need adjustment. With `mock_mission_chain()`, the chain is patched at the prompt level — the model's `with_structured_output` is still called but the resulting chain is replaced. Tests should verify behavior through the mock_chain's `invoke` calls instead.

The auth error and transient error tests that use `mock_structured.invoke.side_effect` need a different approach: either patch `with_llm_retry` to simulate the retry behavior, or set `mock_chain.invoke.side_effect` within the `mock_mission_chain` context.

---

## Fix 3: Property test flakiness

**Tests resolved:** 1 (`test_mission_validation_filerefs.py::TestValidateFileRefsPropertyBased::test_line_clamping_invariant`)

**Root cause:** This test failed in the full suite run but passed when run individually. This is either:
- A Hypothesis database artifact (cached failing example from a previous code state)
- A test ordering dependency (shared state from an earlier test)

### Fix approach

1. Clear the Hypothesis database: `rm -rf .hypothesis/`
2. Run the full suite again to see if it reproduces
3. If it reproduces, investigate test ordering with `pytest --randomly-seed=...`
4. If it doesn't reproduce, it was a stale Hypothesis example — no code change needed

---

## Fix 4: Clean up conftest.py helpers

**Not a test failure**, but architectural cleanup identified during investigation.

`conftest.py` currently has three mock helpers with overlapping purposes:
- `mock_chat_model(response)` — broken with LangChain pipe operator
- `mock_briefing_chain(response)` — works correctly for briefing tests
- `mock_mission_chain(response)` — works correctly for mission tests

### Action

After Fix 2, `mock_chat_model()` is no longer used by any test. Either:
- **Delete it** if no tests reference it (check `test_llm_briefing.py` and `test_llm_provider.py`)
- **Keep it** only if some tests (e.g., provider factory tests that don't use pipe) still need a simple mock model

Check whether `test_llm_briefing.py` uses `mock_chat_model()` or `mock_briefing_chain()`. If it uses `mock_chat_model()` and passes, the briefing generator may not use pipe operator internally — verify before removing.

---

## Execution order

1. **Fix 1** (chmod removal) — source changes first, then test changes
2. **Fix 3** (clear Hypothesis DB) — quick, no code change
3. **Fix 2** (mock setup) — largest change, 19 tests to update
4. **Fix 4** (conftest cleanup) — after Fix 2, assess what's unused
5. Full suite run to verify 0 failures
