# Task Recon: Implement TOML loading/saving with tomllib and tomlkit

**Task ID:** 4.1
**Status:** pending
**Generated:** 2026-01-28T21:30:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Implement the core `load_config()` and `save_config()` methods for the ConfigManager class using `tomllib` (Python 3.11+ stdlib) for reading TOML files and `tomlkit` for writing TOML files while preserving formatting and comments.

### Parent Task Context

**Parent Task:** Implement ConfigManager (ID: 4)
**Parent Goal:** Create the ConfigManager infrastructure component for reading/writing TOML configuration and managing tenant profiles. This component sits in the infrastructure layer and is used by TenantService and AuthService for configuration access.
**This Subtask's Role:** Establishes the foundational file I/O operations (`load_config`, `save_config`, `ensure_config_dir`) that all subsequent ConfigManager methods (tenant CRUD, default tenant management) will depend on. Subtasks 4.2 and 4.3 build directly on this work.

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

### Acceptance Criteria
From task details:
1. Create `src/teams_phone/infrastructure/config_manager.py` with ConfigManager class
2. Implement `__init__` accepting optional `config_path` (defaulting to `DEFAULT_CONFIG_PATH`)
3. Implement `load_config()` using tomllib to parse TOML, validate with Config Pydantic model
4. Handle missing files by returning a default Config instance
5. Implement `save_config()` using tomlkit to serialize Config while preserving formatting
6. Catch `TOMLDecodeError` and pydantic `ValidationError`, wrap in `ConfigurationError` with remediation
7. Use `ensure_config_dir()` to create parent directories before writing

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 2 | Constants and Exception Hierarchy | `ConfigurationError` has exit_code 7, uses keyword-only `remediation` parameter |
| 3 | Pydantic Models for Configuration | `Config` model has nested `CacheSettings` and `NetworkSettings`, uses `extra="forbid"` |

### Reusable Patterns from Dependencies
- **Exception with remediation**: Pattern in `src/teams_phone/exceptions.py:26` - use `ConfigurationError(message, remediation="...")`
- **Pydantic validation**: Use `Config.model_validate()` for dict-to-model conversion
- **Default factory pattern**: `Config()` creates sensible defaults automatically

### Warnings and Gotchas
- **Mypy strict mode**: All functions need complete type hints. The `exit_code` class attribute is typed as `int`.
- **Remediation formatting**: The `__str__` method adds "\n\nTo fix:\n" prefix - don't include that in the remediation string itself.
- **Config model may need `populate_by_name=True`** if TOML uses different casing (flagged by Task 3)
- **datetime.utcnow() is deprecated** in Python 3.12+; use `datetime.now(timezone.utc)`

### Open Items Inherited
- [ ] Config model may need `populate_by_name=True` if TOML uses different casing
- [ ] Consider adding validation for cert_path existence when auth_method is CERTIFICATE

---

## Architecture Context

### Relevant Design Decisions
- **ConfigManager is lowest-level infrastructure**: No dependencies on other components (`.taskmaster/docs/architecture/5-components.md:164`)
- **Technology stack**: tomli/tomllib for reading, tomlkit for writing (preserves formatting)
- **Config location**: `~/.teams-phone/config.toml`

### Module Structure
```
src/teams_phone/
├── infrastructure/
│   ├── __init__.py          # Re-export ConfigManager
│   └── config_manager.py    # NEW - This task
├── models/
│   └── config.py            # Config, CacheSettings, NetworkSettings
├── constants.py             # DEFAULT_CONFIG_PATH
└── exceptions.py            # ConfigurationError
```

### Conventions to Follow
- Use `ConfigDict(extra="forbid")` for all Pydantic models (already done in Config)
- All errors must include remediation guidance
- Use `Path` objects for file operations
- Expand `~` paths using `Path.expanduser()`

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/config.py` | 46-69 | Config model structure with all fields and defaults |
| `src/teams_phone/constants.py` | 25-26 | DEFAULT_CONFIG_PATH = "~/.teams-phone/config.toml" |
| `src/teams_phone/exceptions.py` | 112-122 | ConfigurationError class definition |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/tenant.py` | 22-47 | TenantProfile model (nested in Config.tenants) |
| `.taskmaster/docs/cli-specification/configuration-file.md` | 1-34 | Example TOML structure to support |
| `.taskmaster/docs/architecture/5-components.md` | 152-167 | ConfigManager interface specification |

### Code Patterns to Follow

```python
# Exception pattern from src/teams_phone/exceptions.py:26
class ConfigurationError(TeamsPhoneError):
    exit_code: int = ExitCode.CONFIG_ERROR

# Usage: raise ConfigurationError("message", remediation="guidance")
```

```python
# Config model usage from src/teams_phone/models/config.py:46
from teams_phone.models import Config, TenantProfile

# Create default config
config = Config()

# Access nested settings
print(config.cache.cache_path)  # "~/.teams-phone/cache"
print(config.network.timeout)   # 30

# Validate from dict
config = Config.model_validate(data_dict)
```

---

## Dependencies

### Internal Modules
- `teams_phone.constants` - `DEFAULT_CONFIG_PATH` constant
- `teams_phone.models` - `Config` Pydantic model
- `teams_phone.exceptions` - `ConfigurationError` exception

### External Packages
- `tomllib` (stdlib Python 3.11+) - TOML parsing
- `tomlkit` - TOML writing with formatting preservation
- `pydantic` - Model validation (already in dependencies)
- `pathlib` (stdlib) - Path operations

### New Dependencies Needed
- `tomlkit>=0.12.0` - Must be added to `pyproject.toml` dependencies

---

## Testing Guidance

### Test File Location
`tests/unit/test_config_manager.py`

### Test Patterns in Use
```python
# Example test structure from tests/unit/test_models.py:351
class TestConfig:
    """Tests for Config model."""

    @pytest.mark.unit
    def test_default_values(self) -> None:
        """Config uses default values when not provided."""
        config = Config()
        assert config.config_path == "~/.teams-phone/config.toml"
```

### Fixtures Available
- `tmp_path` (pytest built-in) - Temporary directory for test files

### Test Utilities
- `tests/conftest.py` - Marker registration (unit, integration, contract)

### Test Strategy from Task Details
1. Test load nonexistent file returns default Config
2. Test load valid TOML parses correctly
3. Test load invalid TOML raises ConfigurationError
4. Test save_config writes valid TOML that can be round-tripped
5. Test formatting preservation by checking tomlkit output preserves comments

---

## Risks and Blockers

### Technical Risks
- [ ] **TOML key naming**: Config model uses snake_case but example TOML in spec uses snake_case too - should be compatible. Verify with round-trip test.
- [ ] **UUID serialization**: TenantProfile has UUID fields that need proper serialization to TOML strings. tomlkit may need explicit string conversion.
- [ ] **Enum serialization**: AuthMethod enum needs to serialize as its string value ("certificate", "client-secret").

### Dependency Risks
- [ ] **tomlkit not in pyproject.toml**: Must add `tomlkit>=0.12.0` to dependencies before implementation.

### Open Questions
- [ ] Should `load_config()` cache the loaded config or read from disk every time? (Task details suggest per-call loading for atomic operations in subtask 4.2)
- [ ] Should `save_config()` preserve existing comments in the file, or can it overwrite completely? (tomlkit can do either)

---

## Implementation Recommendations

### Suggested Approach
1. Add `tomlkit>=0.12.0` to `pyproject.toml` dependencies
2. Create `src/teams_phone/infrastructure/config_manager.py` with class skeleton
3. Implement `ensure_config_dir()` first (used by save_config)
4. Implement `load_config()` with:
   - Path expansion (`~/.teams-phone/config.toml` -> absolute)
   - FileNotFoundError handling (return default Config)
   - tomllib.loads() for parsing
   - Pydantic validation with Config.model_validate()
   - TOMLDecodeError/ValidationError wrapping
5. Implement `save_config()` with:
   - Call ensure_config_dir()
   - Convert Config to dict with model_dump()
   - Use tomlkit.dumps() to serialize
   - Write atomically (write to temp, rename)
6. Update `infrastructure/__init__.py` to export ConfigManager
7. Write comprehensive unit tests

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/infrastructure/config_manager.py` | Main implementation |
| Modify | `src/teams_phone/infrastructure/__init__.py` | Export ConfigManager |
| Modify | `pyproject.toml` | Add tomlkit dependency |
| Create | `tests/unit/test_config_manager.py` | Unit tests |

### Quick Wins
- Start with `ensure_config_dir()` - simplest method, tests Path.mkdir()
- Then `load_config()` with missing file case - validates default Config creation

---

## Cheat Sheet

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

from teams_phone.constants import DEFAULT_CONFIG_PATH
from teams_phone.exceptions import ConfigurationError
from teams_phone.models import Config
```

### Key Functions to Use
- `Path.expanduser()` - Expand `~` to home directory
- `Path.mkdir(parents=True, exist_ok=True)` - Create directories
- `tomllib.loads(content)` - Parse TOML string
- `tomlkit.dumps(data)` - Serialize to TOML string
- `Config.model_validate(data)` - Validate dict to Config
- `config.model_dump(mode="json")` - Serialize Config to JSON-compatible dict (handles UUID/Enum)

### Common Patterns
- **Error wrapping**: See `src/teams_phone/exceptions.py:112-122`
- **Pydantic strict validation**: See `src/teams_phone/models/config.py:61`
- **Test with tmp_path**: See `tests/unit/test_models.py` for fixture usage

### TOML Structure Reference
```toml
# From .taskmaster/docs/cli-specification/configuration-file.md
[default]
tenant = "contoso"

[tenants.contoso]
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
display_name = "Contoso Ltd"
auth_method = "certificate"
cert_path = "/path/to/contoso-cert.pem"
```

---

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