# Task Recon: Implement token storage and retrieval with JSON persistence

**Task ID:** 5.1
**Status:** pending
**Generated:** 2026-01-28T18:45:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the CacheManager class with token management methods for JSON-based token caching. Implement `get_token`, `save_token`, and `clear_tokens` methods that persist CachedToken objects to the token directory.

### Parent Task Context

**Parent Task:** Implement CacheManager (ID: 5)
**Parent Goal:** Create the CacheManager for token caching, CSV file reading, and response caching with TTL support.
**This Subtask's Role:** This subtask establishes the foundational CacheManager class structure with token persistence, which subsequent subtasks (5.2, 5.3) will extend with CSV reading and staleness detection capabilities.

### Dependencies
- Depends on: Task 2 (Constants and Exception Hierarchy) - **DONE**
- Depends on: Task 3 (Pydantic Models for Configuration) - **DONE**

### Acceptance Criteria
1. CacheManager class with `cache_dir` and `token_dir` parameters (defaulting to constants)
2. `ensure_token_dir()` helper creates directory with parents
3. `get_token(tenant_name)` reads JSON and deserializes to CachedToken, returns None if missing/invalid
4. `save_token(tenant_name, token)` writes token to JSON using Pydantic serialization
5. `clear_tokens(tenant_name)` deletes specific or all token files
6. PermissionError raises ConfigurationError with remediation guidance

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 2 | Constants and Exception Hierarchy | `DEFAULT_TOKEN_PATH = "~/.teams-phone/tokens"` and `ConfigurationError` with remediation support are available |
| 3 | Pydantic Models for Configuration | `CachedToken` model has `model_validate_json()` for deserialization and `model_dump_json()` for serialization |

### Reusable Patterns from Dependencies
- **ConfigManager.ensure_config_dir()**: Pattern for directory creation with PermissionError handling in `src/teams_phone/infrastructure/config_manager.py:40-57`
- **CachedToken Pydantic methods**: Use `CachedToken.model_validate_json()` to parse JSON strings and `token.model_dump_json()` to serialize

### Warnings and Gotchas
- **Mypy strict mode**: All functions need complete type hints. The `exit_code` class attribute is typed as `int` (not `ExitCode`) because IntEnum values are ints.
- **Remediation formatting**: Exception `__str__` method adds "\n\nTo fix:\n" prefix - don't include that in the remediation string itself.
- **datetime.utcnow() deprecated**: Use `datetime.now(timezone.utc)` instead (Python 3.12+)
- **CachedToken.is_expired()**: Already handles both naive and aware datetimes for comparison

### Open Items Inherited
- [ ] (From Task 2) Exception subclasses (TokenExpiredError, UserNotFoundError, etc.) to be added when specific features need them
- [ ] (From Task 3) Config model may need `populate_by_name=True` if TOML uses different casing

---

## Architecture Context

### Relevant Design Decisions
- **CacheManager is lowest-level infrastructure** - has no dependencies on other components (see architecture doc 5-components.md)
- **Token files stored as JSON** - one file per tenant in `~/.teams-phone/tokens/{tenant_name}.json`
- **Graceful degradation** - return None/empty list when files don't exist (no errors for missing cache)

### Module Structure
```
src/teams_phone/
├── infrastructure/
│   ├── __init__.py           # Re-exports CacheManager (update needed)
│   ├── config_manager.py     # Existing pattern reference
│   └── cache_manager.py      # NEW FILE - this task
├── constants.py              # DEFAULT_TOKEN_PATH, DEFAULT_CACHE_PATH
├── exceptions.py             # ConfigurationError (with remediation)
└── models/
    └── auth.py               # CachedToken model
```

### Conventions to Follow
- Use `ConfigDict(extra="forbid")` for Pydantic models (already in CachedToken)
- Docstrings follow Google style (see config_manager.py)
- Type hints required for all functions (mypy strict mode)
- Use `Path.expanduser()` for tilde expansion
- Use keyword-only `remediation` parameter in exceptions

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/config_manager.py` | 40-57 | `ensure_config_dir()` pattern with PermissionError handling |
| `src/teams_phone/infrastructure/config_manager.py` | 30-38 | `__init__` pattern with Path expansion |
| `src/teams_phone/models/auth.py` | 24-55 | `CachedToken` model structure and methods |
| `src/teams_phone/constants.py` | 25-31 | Default paths (DEFAULT_CACHE_PATH, DEFAULT_TOKEN_PATH) |
| `src/teams_phone/exceptions.py` | 112-121 | `ConfigurationError` class with remediation |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/__init__.py` | 1-6 | Re-export pattern - needs update to include CacheManager |
| `tests/unit/test_config_manager.py` | 43-78 | Test pattern for ensure_dir and permission errors |
| `tests/unit/test_models.py` | 181-232 | Test patterns for CachedToken |

### Code Patterns to Follow

```python
# Directory creation pattern (from config_manager.py:40-57)
def ensure_token_dir(self) -> None:
    """Create the token directory if it doesn't exist."""
    try:
        self._token_dir.mkdir(parents=True, exist_ok=True)
    except PermissionError as e:
        raise ConfigurationError(
            f"Cannot create token directory {self._token_dir}: {e}",
            remediation=(
                f"Check that you have write permissions to {self._token_dir}.\n"
                "You may need to create the directory manually or run with elevated privileges."
            ),
        ) from e
```

```python
# Path expansion pattern (from config_manager.py:30-38)
def __init__(self, cache_dir: Path | None = None, token_dir: Path | None = None) -> None:
    path_str = cache_dir if cache_dir is not None else DEFAULT_CACHE_PATH
    self._cache_dir: Path = Path(path_str).expanduser()
    token_str = token_dir if token_dir is not None else DEFAULT_TOKEN_PATH
    self._token_dir: Path = Path(token_str).expanduser()
```

```python
# Pydantic JSON serialization (from CachedToken model)
# Save: token.model_dump_json()
# Load: CachedToken.model_validate_json(json_string)
```

---

## Dependencies

### Internal Modules
- `teams_phone.constants` - DEFAULT_CACHE_PATH, DEFAULT_TOKEN_PATH
- `teams_phone.exceptions` - ConfigurationError (with remediation support)
- `teams_phone.models` - CachedToken (Pydantic model)

### External Packages
- `pathlib.Path` - stdlib, for path operations
- `pydantic` - already installed (2.0.0+), for model_validate_json/model_dump_json

### New Dependencies Needed
- None required - all needed functionality available in stdlib and existing dependencies

---

## Testing Guidance

### Test File Location
`tests/unit/test_cache_manager.py` (NEW FILE)

### Test Patterns in Use
```python
# From tests/unit/test_config_manager.py:46-54
class TestEnsureTokenDir:
    @pytest.mark.unit
    def test_creates_parent_directories(self, tmp_path: Path) -> None:
        """ensure_token_dir creates parent directories."""
        token_path = tmp_path / "deep" / "nested" / "tokens"
        manager = CacheManager(token_dir=str(token_path))

        assert not token_path.exists()
        manager.ensure_token_dir()
        assert token_path.exists()
```

```python
# From tests/unit/test_config_manager.py:66-78
@pytest.mark.unit
def test_permission_error_raises_configuration_error(self, tmp_path: Path) -> None:
    """ensure_token_dir raises ConfigurationError on permission failure."""
    token_path = tmp_path / "restricted" / "tokens"
    manager = CacheManager(token_dir=str(token_path))

    with patch.object(Path, "mkdir", side_effect=PermissionError("Access denied")):
        with pytest.raises(ConfigurationError) as exc_info:
            manager.ensure_token_dir()

    assert "Cannot create token directory" in str(exc_info.value)
    assert exc_info.value.remediation is not None
```

### Fixtures Available
- `tmp_path` - pytest built-in fixture for temporary directories (used in all existing tests)

### Test Utilities
- `unittest.mock.patch` - for mocking Path.mkdir, file operations
- `pytest.raises` - for exception testing
- `@pytest.mark.unit` - marker for unit tests

---

## Risks and Blockers

### Technical Risks
- [ ] **JSON encoding edge cases**: CachedToken.expires_at is a datetime - verify model_dump_json() handles ISO format correctly
- [ ] **File locking on Windows**: Concurrent token writes could conflict - consider atomic write pattern if needed (not required for MVP)

### Dependency Risks
- None - all dependencies (Task 2, Task 3) are complete

### Open Questions
- [ ] Should `get_token` log a warning when JSON parsing fails, or silently return None? (Subtask details say "return None if file doesn't exist or is invalid JSON" - silent seems preferred)
- [ ] Should `clear_tokens(None)` delete the entire tokens directory or just all .json files? (Subtask says "all `.json` files in token_dir" - keep directory)

---

## Implementation Recommendations

### Suggested Approach
1. Create `src/teams_phone/infrastructure/cache_manager.py` with class skeleton
2. Implement `__init__` with Path expansion (mirror ConfigManager pattern)
3. Implement `ensure_token_dir()` copying ConfigManager.ensure_config_dir() pattern
4. Implement `_get_token_path(tenant_name)` helper for file path construction
5. Implement `get_token()` with file read and model_validate_json
6. Implement `save_token()` with ensure_token_dir and model_dump_json
7. Implement `clear_tokens()` with Path.unlink and glob pattern
8. Update `src/teams_phone/infrastructure/__init__.py` to export CacheManager
9. Create comprehensive unit tests following test_config_manager.py patterns

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/infrastructure/cache_manager.py` | Main implementation |
| Modify | `src/teams_phone/infrastructure/__init__.py` | Add CacheManager to exports |
| Create | `tests/unit/test_cache_manager.py` | Unit tests |

### Quick Wins
- Start with `ensure_token_dir()` since it's a direct copy of the ConfigManager pattern
- Then `save_token()` and `get_token()` are straightforward Pydantic serialization
- `clear_tokens()` is simple Path.unlink/glob operations

---

## Cheat Sheet

### Key Imports
```python
from pathlib import Path
from pydantic import ValidationError as PydanticValidationError

from teams_phone.constants import DEFAULT_CACHE_PATH, DEFAULT_TOKEN_PATH
from teams_phone.exceptions import ConfigurationError
from teams_phone.models import CachedToken
```

### Key Functions to Use
- `Path.expanduser()` - expand ~ in paths
- `Path.mkdir(parents=True, exist_ok=True)` - create directories
- `Path.exists()` - check if file exists
- `Path.read_text(encoding="utf-8")` - read file content
- `Path.write_text(content, encoding="utf-8")` - write file content
- `Path.unlink(missing_ok=True)` - delete file (Python 3.8+)
- `Path.glob("*.json")` - iterate over matching files
- `CachedToken.model_validate_json(json_str)` - parse JSON to model
- `token.model_dump_json()` - serialize model to JSON string

### Common Patterns
- **Directory creation**: See `config_manager.py:40-57`
- **Path expansion**: See `config_manager.py:37-38`
- **PermissionError handling**: Catch and re-raise as ConfigurationError with remediation
- **Pydantic validation errors**: Catch `PydanticValidationError` (aliased import to avoid conflict with our ValidationError)

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
