# Task Recon: Implement numbers and policies CLI integration tests with global flags and error scenarios

**Task ID:** 35.4
**Status:** pending
**Generated:** 2026-01-30T22:30:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create integration tests for numbers commands (list/show/search/assign/unassign with filters), policies commands (list/assign), global flags (--tenant, --verbose, --no-color), and comprehensive error scenarios.

### Parent Task Context

**Parent Task:** Write CLI Integration Tests (ID: 35)
**Parent Goal:** Create integration tests for CLI commands using CliRunner with mocked services, testing all commands with default (table) output, JSON output, error cases (not found, validation), and global flags (--tenant, --verbose).
**This Subtask's Role:** Complete the CLI integration test suite by covering the numbers and policies commands, global flag behavior, and comprehensive error scenario testing across all exception types.

### Dependencies
- Depends on: 35.1 (Create CLI integration test infrastructure with conftest.py and shared fixtures)

### Acceptance Criteria
- Create `tests/integration/cli/test_numbers_integration.py` with tests for:
  - `TestNumbersListIntegration`: list with filters (--status, --type, --city), JSON output
  - `TestNumbersShowIntegration`: show details, not found error (exit code 4)
  - `TestNumbersSearchIntegration`: pattern matching
  - `TestNumbersAssignIntegration`: success with 202 polling, --dry-run, missing location (exit code 5), not found (exit code 4)
  - `TestNumbersUnassignIntegration`: success, --dry-run
- Create `tests/integration/cli/test_policies_integration.py` with tests for:
  - `TestPoliciesListIntegration`: list all, filter by type, JSON output
  - `TestPoliciesShowIntegration`: show user policies
  - `TestPoliciesAssignIntegration`: success, --type option
- Create `tests/integration/cli/test_global_options.py` with tests for:
  - `TestGlobalOptions`: --tenant, --verbose, --no-color, --json, combined flags
  - `TestErrorScenarios`: exit codes for NotFoundError(4), ValidationError(5), AuthenticationError(2), ConfigurationError(7), ContractError(8)
- Run `pytest tests/integration/cli/ -v --cov=teams_phone.cli --cov-report=term-missing` with >85% coverage

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 35.1 | Create CLI integration test infrastructure | Composite fixtures return dict of mocks keyed by service name; policies CLI patches `_get_sync_service` not the service class directly |
| 35.2 | Auth and tenants CLI integration tests | ANSI escape codes appear with numeric values; use `--force` to bypass confirmation prompts; Connection test JSON mode returns exit 0 even on failure |
| 35.3 | Users and locations CLI integration tests | Use short unique display names to avoid table truncation; staleness warnings have numeric ANSI codes; exit codes 4=NotFound, 5=Validation |

### Reusable Patterns from Dependencies
- Table output tests: Use short, non-numeric test data to avoid ANSI/truncation issues
- JSON validation: Use `json.loads(result.stdout)` and assert structure
- Exit code assertions: Map exceptions to exit codes (NotFoundError=4, ValidationError=5)
- Composite fixture usage: `mocks = mock_cli_services_numbers; mocks["number_service"].list_numbers.return_value = [...]`

### Warnings and Gotchas
- **From Task 35.1**: AsyncMock `__aenter__` must return self for context manager pattern
- **From Task 35.1**: Policies CLI patches `_get_sync_service` not the service class directly
- **From Task 35.2**: Table output with numeric values may contain ANSI escape codes
- **From Task 35.3**: Rich table columns truncate long values with ellipsis - use short test data
- **Numbers CLI**: Uses async operations with GraphClient context manager - requires proper AsyncMock setup
- **Assign/Unassign**: Return AsyncOperation which requires proper mock setup with OperationStatus

### Open Items Inherited
- None directly inherited - this is the final subtask for CLI integration tests

---

## Architecture Context

### Relevant Design Decisions
- **Layered Architecture**: CLI layer imports services, services handle Graph API/cache access
- **Exit Code Strategy**: Each exception type maps to a specific exit code (2-8) per `teams_phone/exceptions.py`
- **Async Operations**: Number assign/unassign return 202 and require polling via AsyncOperation

### Module Structure
```
tests/integration/cli/
├── conftest.py                    # Existing - fixtures
├── test_auth_integration.py       # Done - 35.2
├── test_tenants_integration.py    # Done - 35.2
├── test_users_integration.py      # Done - 35.3
├── test_locations_integration.py  # Done - 35.3
├── test_numbers_integration.py    # TO CREATE
├── test_policies_integration.py   # TO CREATE
└── test_global_options.py         # TO CREATE
```

### Conventions to Follow
- Use `@pytest.mark.integration` decorator on all tests
- Class-based test organization (e.g., `TestNumbersListIntegration`)
- Import model factories from `tests.integration.cli.conftest`
- Use composite fixtures for patching (e.g., `mock_cli_services_numbers`)

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `C:\Users\frank\Projects\teams-phone\tests\integration\cli\conftest.py` | 596-624 | `mock_cli_services_numbers` fixture - how to patch numbers CLI dependencies |
| `C:\Users\frank\Projects\teams-phone\tests\integration\cli\conftest.py` | 651-668 | `mock_cli_services_policies` fixture - patches `_get_sync_service` |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\cli\numbers.py` | 293-377 | `list_numbers` command - filter options (--status, --type, --city, --limit, --all) |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\cli\numbers.py` | 547-757 | `assign_number` command - dry-run, location validation, async operation |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\cli\policies.py` | 195-256 | `list_policies` command - policy_type filter, staleness check |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\cli\policies.py` | 322-455 | `assign_policy` command - user resolution, policy validation, dry-run |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\exceptions.py` | 1-138 | Exception hierarchy with exit codes (2-8) |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `C:\Users\frank\Projects\teams-phone\tests\integration\cli\test_users_integration.py` | 22-258 | Pattern for list/show/search tests with filters |
| `C:\Users\frank\Projects\teams-phone\tests\integration\cli\test_locations_integration.py` | 22-177 | Pattern for cache-based service tests with staleness warnings |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\cli\main.py` | 32-87 | Global options callback (--json, --verbose, --tenant, --no-color) |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\constants.py` | 55-97 | ExitCode enum - all exit codes defined |

### Code Patterns to Follow

```python
# Pattern from test_users_integration.py:22-45 - List command test
class TestNumbersListIntegration:
    @pytest.mark.integration
    def test_list_all_numbers_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """List without filters returns all numbers in table format."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(telephone_number="+14255551234"),
            make_number(telephone_number="+14255555678"),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

        result = runner.invoke(app, ["numbers", "list"])
        assert result.exit_code == 0
        assert "+14255551234" in result.stdout
```

```python
# Pattern from test_locations_integration.py:241-257 - NotFoundError test
@pytest.mark.integration
def test_show_number_not_found(
    self,
    runner: CliRunner,
    mock_cli_services_numbers: dict[str, MagicMock],
) -> None:
    """NotFoundError results in exit code 4."""
    mocks = mock_cli_services_numbers
    mocks["number_service"].get_number.side_effect = NotFoundError(
        "Phone number '+19999999999' not found",
        remediation="Verify the phone number is in your inventory.",
    )

    result = runner.invoke(app, ["numbers", "show", "+19999999999"])
    assert result.exit_code == 4
    assert "not found" in result.stdout.lower()
```

```python
# Pattern for assign with AsyncOperation mock
from datetime import datetime, timezone
from teams_phone.models import AsyncOperation, OperationStatus

@pytest.mark.integration
def test_assign_number_success(
    self,
    runner: CliRunner,
    mock_cli_services_numbers: dict[str, MagicMock],
) -> None:
    """Successful assignment with 202 polling."""
    mocks = mock_cli_services_numbers
    operation = AsyncOperation(
        id="op-123",
        created_date_time=datetime.now(timezone.utc),
        status=OperationStatus.SUCCEEDED,
    )
    mocks["number_service"].assign_number.return_value = operation
    # Also need user service mock for resolve_user
    # ... setup user mock

    result = runner.invoke(app, ["numbers", "assign", "user@contoso.com", "+14255551234", "--force"])
    assert result.exit_code == 0
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.main` - Main app with global options
- `teams_phone.cli.numbers` - Numbers command group
- `teams_phone.cli.policies` - Policies command group
- `teams_phone.exceptions` - All exception types with exit codes
- `teams_phone.models` - NumberAssignment, AsyncOperation, Policy, OperationStatus enums

### External Packages
- `pytest` - Test framework with markers
- `typer.testing.CliRunner` - CLI invocation for testing
- `unittest.mock` - MagicMock, AsyncMock, patch

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Locations
- `tests/integration/cli/test_numbers_integration.py` (to create)
- `tests/integration/cli/test_policies_integration.py` (to create)
- `tests/integration/cli/test_global_options.py` (to create)

### Test Patterns in Use
```python
# Pattern from existing tests - composite fixture usage
def test_example(
    self,
    runner: CliRunner,
    mock_cli_services_numbers: dict[str, MagicMock],
) -> None:
    mocks = mock_cli_services_numbers
    # Configure return values
    mocks["number_service"].list_numbers.return_value = [...]
    # Invoke command
    result = runner.invoke(app, ["numbers", "list"])
    # Assert
    assert result.exit_code == 0
```

### Fixtures Available
- `runner` - CliRunner instance
- `mock_cli_services_numbers` - Patches CacheManager, AuthService, GraphClient, NumberService
- `mock_cli_services_policies` - Patches via `_get_sync_service` helper
- `make_number()` - Factory for NumberAssignment
- `make_policy()` - Factory for Policy
- `make_user_config()` - Factory for UserConfiguration (needed for assign tests)
- `make_location()` - Factory for EmergencyLocation (needed for assign tests)

### Test Utilities
- `tests/integration/cli/conftest.py` - All fixtures and factories

---

## Risks and Blockers

### Technical Risks
- [ ] **Assign command complexity**: Requires mocking multiple services (UserService, NumberService, LocationService) and AsyncOperation result - use existing pattern from `_run_assign_async` helper
- [ ] **Policies patching**: Uses `_get_sync_service` helper, not direct service patching - the existing `mock_cli_services_policies` fixture handles this correctly

### Dependency Risks
- None - 35.1 is complete with all required fixtures

### Open Questions
- None identified - task details are comprehensive

---

## Implementation Recommendations

### Suggested Approach
1. Create `test_numbers_integration.py` first (most complex due to async operations)
2. Create `test_policies_integration.py` (simpler, sync/cache-based)
3. Create `test_global_options.py` (cross-cutting concerns)
4. Run full test suite with coverage to verify >85%

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `tests/integration/cli/test_numbers_integration.py` | Numbers CLI tests (list/show/search/assign/unassign) |
| Create | `tests/integration/cli/test_policies_integration.py` | Policies CLI tests (list/show/assign) |
| Create | `tests/integration/cli/test_global_options.py` | Global flags and error scenario tests |

### Quick Wins
- Start with `test_numbers_list_json_output` - simple test that validates JSON structure
- Reuse patterns from `test_users_integration.py` for list/show/search commands
- Use `--force` flag to bypass confirmation prompts in assign/unassign tests

---

## Cheat Sheet

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

import json
from datetime import datetime, timezone
from unittest.mock import MagicMock

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import (
    AuthenticationError,
    ConfigurationError,
    ContractError,
    NotFoundError,
    ValidationError,
)
from teams_phone.models import (
    AssignmentStatus,
    NumberType,
    OperationStatus,
)

from tests.integration.cli.conftest import (
    make_number,
    make_policy,
    make_user_config,
    make_location,
)
```

### Key Functions to Use
- `make_number(telephone_number=..., assignment_status=..., number_type=...)` - Create NumberAssignment
- `make_policy(policy_type=..., policy_name=..., is_global=...)` - Create Policy
- `runner.invoke(app, ["command", "args", "--options"])` - Invoke CLI command
- `json.loads(result.stdout)` - Parse JSON output

### Common Patterns
- Exit codes: `result.exit_code == 0` (success), `4` (NotFound), `5` (Validation), `2` (Auth), `7` (Config), `8` (Contract)
- JSON flag: `["--json", "numbers", "list"]` - global flag before subcommand
- Force flag: `["numbers", "assign", "user", "number", "--force"]` - skip confirmation
- Dry run: `["numbers", "assign", "user", "number", "--dry-run"]` - preview without changes

### Exception-to-Exit-Code Mapping
| Exception | Exit Code | Test Pattern |
|-----------|-----------|--------------|
| `NotFoundError` | 4 | `mocks["service"].method.side_effect = NotFoundError("message")` |
| `ValidationError` | 5 | `mocks["service"].method.side_effect = ValidationError("message")` |
| `AuthenticationError` | 2 | `mocks["auth_service"].login.side_effect = AuthenticationError("message")` |
| `ConfigurationError` | 7 | `mocks["cache_manager"].side_effect = ConfigurationError("message")` |
| `ContractError` | 8 | `mocks["service"].method.side_effect = ContractError("message")` |

---

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