# Task Recon: Implement Locations CLI Commands

**Task ID:** 16
**Status:** pending
**Generated:** 2026-01-29T23:55:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the `locations` command group with `list`, `show`, and `search` commands that read emergency location data from the CSV cache (not Graph API). These commands must display staleness warnings when the cache is more than 30 days old.

### Dependencies
- Task 8: Implement OutputFormatter (done)
- Task 11: Implement Root CLI App with Global Options (done)
- Task 15: Implement LocationService (done)
- Task 39: Validation Checkpoint 1 (done)

### Acceptance Criteria
- `locations list` command lists all emergency locations with filtering options
- `locations show <location>` command displays detailed information for a specific location
- `locations search <query>` command searches locations by name, city, or address
- Staleness warning displayed when cache is older than 30 days
- JSON output mode supported via global `--json` flag
- Table output uses Rich formatting

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 8 | OutputFormatter | JSON mode uses plain `print()` to stdout, not Rich's `print_json()`. NO_COLOR respects both flag and env var. |
| 11 | Root CLI App | `locations_app` placeholder already exists in `main.py`. Replace it with import from `locations.py`. CLIContext provides type-safe access via `get_context(ctx)`. |
| 15 | LocationService | CSV `isDefault` field is a string that gets converted to bool. Service methods: `list_locations()`, `get_location()`, `search_locations()`, `validate_location()`, `get_staleness_warning()`. |

### Reusable Patterns from Dependencies
- **CLI command structure**: Follow `users.py` pattern for command definitions and error handling (`src/teams_phone/cli/users.py:151-234`)
- **Output formatting**: Use `formatter.table()`, `formatter.json()`, `formatter.info()`, `formatter.warning()` from OutputFormatter (`src/teams_phone/infrastructure/output_formatter.py:61-143`)
- **Context retrieval**: Use `get_context(ctx)` for type-safe access to CLIContext (`src/teams_phone/cli/context.py:76-93`)
- **Error handling pattern**: Wrap service calls in try/except for `TeamsPhoneError` (`src/teams_phone/cli/users.py:202-233`)

### Warnings and Gotchas
- **Exit code for validation errors**: Use exit code 5 for validation errors (e.g., `--limit` and `--all` together)
- **Sub-app registration**: Must update `main.py` to import the new `locations_app` instead of using the placeholder
- **No async required**: LocationService is synchronous (reads from CSV cache), unlike UserService which is async
- **JSON output for empty results**: Still output valid JSON (empty array) when no results found

### Open Items Inherited
- None from dependency tasks

---

## Architecture Context

### Relevant Design Decisions
- **CSV Cache Pattern**: Locations are read from `~/.teams-phone/cache/locations.csv` since Graph API does not expose location endpoints
- **Staleness Detection**: Cache older than 30 days (`CSV_STALE_DAYS`) triggers warning before output
- **Service Layer**: Commands call LocationService methods, not CacheManager directly

### Module Structure
```
src/teams_phone/cli/locations.py  # New file to create
  - locations_app: Typer instance
  - list_locations: List all locations
  - show: Show single location details
  - search: Search locations by query
```

### Conventions to Follow
- Use `typer.Typer(name="...", help="...", no_args_is_help=True)` for sub-apps
- Decorate list command with `@locations_app.command("list")` (reserved keyword)
- Use `typer.Argument()` for required positional arguments
- Use `typer.Option()` for optional flags with defaults
- Use `Optional[type]` for nullable options (Python 3.10+ compatible)
- Include type hints for all function parameters and return values
- Add docstrings with imperative sentences

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/users.py` | 1-372 | Reference implementation for CLI commands (list, show, search pattern) |
| `src/teams_phone/services/location_service.py` | 1-195 | LocationService API to use |
| `src/teams_phone/cli/main.py` | 75-101 | Where to register the new locations_app |
| `src/teams_phone/cli/context.py` | 19-93 | CLIContext and get_context() helper |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/location.py` | 1-37 | EmergencyLocation model structure |
| `src/teams_phone/infrastructure/output_formatter.py` | 61-143 | OutputFormatter methods |
| `src/teams_phone/constants.py` | 44-45 | CSV_STALE_DAYS constant |
| `.taskmaster/docs/cli-specification/command-group-6-locations.md` | 1-139 | CLI specification for locations commands |

### Code Patterns to Follow

```python
# Pattern from src/teams_phone/cli/users.py:151-192
@users_app.command("list")
def list_users(
    ctx: typer.Context,
    limit: Optional[int] = typer.Option(
        None,
        "--limit",
        "-l",
        help="Limit results to n users.",
    ),
    all_users: bool = typer.Option(
        False,
        "--all",
        help="Fetch all users (may be slow for large tenants).",
    ),
) -> None:
    """List users with voice configuration."""
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    # Validate mutually exclusive options
    if limit is not None and all_users:
        formatter.error(
            "Cannot specify both --limit and --all",
            remediation="Use either --limit <n> to limit results or --all to fetch all users.",
        )
        raise typer.Exit(5)  # ValidationError exit code
```

```python
# Pattern for staleness warning (LocationService)
staleness_warning = service.get_staleness_warning()
if staleness_warning:
    formatter.warning(staleness_warning)
```

```python
# Pattern for synchronous service call (no async needed)
# From services that don't use GraphClient
cache_manager = CacheManager()
service = LocationService(cache_manager)
locations = service.list_locations()
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.context` - CLIContext and get_context()
- `teams_phone.services.location_service` - LocationService
- `teams_phone.infrastructure` - CacheManager
- `teams_phone.exceptions` - TeamsPhoneError for error handling
- `teams_phone.models` - EmergencyLocation

### External Packages
- `typer` - CLI framework
- `typing` - Optional, TYPE_CHECKING

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_cli_locations.py` (new file to create)

### Test Patterns in Use
```python
# Pattern from tests/unit/test_cli_users.py:80-111
class TestUsersListCommand:
    """Tests for users list command."""

    @pytest.mark.unit
    def test_list_shows_in_help(self) -> None:
        """list command appears in users help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "list" in result.stdout

    @pytest.mark.unit
    def test_list_empty(self) -> None:
        """list shows empty message when no users found."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            ...
        ):
            result = runner.invoke(app, ["users", "list"])
            assert result.exit_code == 0
            assert "No users found" in result.stdout
```

```python
# Simpler pattern for synchronous service mocking (locations)
from unittest.mock import patch, MagicMock

with patch("teams_phone.cli.locations.CacheManager") as mock_cm_class:
    mock_cm = MagicMock()
    mock_cm_class.return_value = mock_cm
    # Configure mock_cm methods as needed

    with patch("teams_phone.cli.locations.LocationService") as mock_ls_class:
        mock_service = MagicMock()
        mock_service.list_locations.return_value = [...]
        mock_service.get_staleness_warning.return_value = None
        mock_ls_class.return_value = mock_service

        result = runner.invoke(app, ["locations", "list"])
```

### Fixtures Available
- `tmp_path` (pytest built-in) - For creating temporary cache directories
- `runner` - `CliRunner()` from `typer.testing`

### Test Utilities
- `typer.testing.CliRunner` - For invoking CLI commands
- `unittest.mock.patch` - For mocking CacheManager and LocationService
- `unittest.mock.MagicMock` - For mock return values

---

## Risks and Blockers

### Technical Risks
- [x] **Low risk**: LocationService is already fully implemented and tested

### Dependency Risks
- [x] All dependencies (Task 8, 11, 15, 39) are complete

### Open Questions
- [ ] Should `locations list --city <city>` filter be implemented? (mentioned in CLI spec but not in task description)
- [ ] Should `locations list --validated` filter be implemented? (mentioned in CLI spec)
- [ ] Should `locations search --field <field>` option be implemented? (mentioned in CLI spec)

---

## Implementation Recommendations

### Suggested Approach
1. Create `src/teams_phone/cli/locations.py` with `locations_app` Typer instance
2. Implement `list` command with `--limit`, `--all`, `--city` (optional) options
3. Implement `show` command with location argument (ID or description)
4. Implement `search` command with query argument and `--limit`, `--field` options
5. Add staleness warning check before output in each command
6. Update `src/teams_phone/cli/main.py` to import and use the new `locations_app`
7. Create `tests/unit/test_cli_locations.py` with comprehensive tests

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/cli/locations.py` | Main implementation |
| Modify | `src/teams_phone/cli/main.py` | Replace placeholder locations_app with import |
| Create | `tests/unit/test_cli_locations.py` | Unit tests |

### Quick Wins
- Start with `list` command since it's the simplest
- Copy structure from `users.py` and adapt for synchronous service calls
- Staleness warning is a single method call: `service.get_staleness_warning()`

---

## Cheat Sheet

### Key Imports
```python
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Optional

import typer

from teams_phone.cli.context import get_context
from teams_phone.exceptions import TeamsPhoneError
from teams_phone.infrastructure import CacheManager
from teams_phone.models import EmergencyLocation
from teams_phone.services import LocationService

if TYPE_CHECKING:
    from teams_phone.infrastructure import OutputFormatter
```

### Key Functions to Use
- `get_context(ctx)` - Get typed CLIContext from Typer context
- `cli_ctx.get_output_formatter()` - Get OutputFormatter instance
- `LocationService(cache_manager)` - Create LocationService
- `service.list_locations()` - Get all locations
- `service.validate_location(identifier)` - Get location by ID or description (raises NotFoundError)
- `service.search_locations(query)` - Search locations
- `service.get_staleness_warning()` - Get warning string or None
- `formatter.table(data, columns, title=...)` - Display table
- `formatter.json(data)` - Output JSON
- `formatter.warning(message)` - Display warning
- `formatter.info(message)` - Display info
- `formatter.error(message, remediation=...)` - Display error

### Common Patterns
- **Context retrieval**: `cli_ctx = get_context(ctx)` at command start
- **Staleness check**: Check and display warning before output
- **Error handling**: `except TeamsPhoneError as e: formatter.error(e.message, remediation=e.remediation); raise typer.Exit(e.exit_code)`
- **Table output**: Convert model to dict with display-friendly values
- **JSON output**: Convert model to dict with raw values

### Display Table Columns (from CLI spec)
- List: description, address, city, state, isDefault
- Show: All fields (location_id, civic_address_id, description, company_name, address, city, state_or_province, postal_code, country_or_region, is_default)

---

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