# Task Recon: Implement execution loop with progress display and results output

**Task ID:** 31.2
**Status:** pending
**Generated:** 2026-01-30
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Implement the async execution loop for policy assignments with Rich progress bar, continue-on-error handling, and CSV/JSON results file output. This completes the `policies bulk-assign` command by adding the actual execution phase after validation passes.

### Parent Task Context
**Parent Task:** Implement Policies Bulk-Assign Command (ID: 31)
**Parent Goal:** Add bulk-assign command to policies CLI for assigning policies to multiple users from CSV.
**This Subtask's Role:** Completes the execution phase - subtask 31.1 implemented CSV parsing and validation; this subtask implements the actual policy assignment loop with progress display and results output.

### Dependencies
- Depends on: 31.1 (CSV parsing and validation - DONE)

### Acceptance Criteria
1. For each validated row, resolve user via `user_service.resolve_user()` and call `policy_service.assign_policy()`
2. Display Rich progress bar with spinner showing current progress
3. When `--continue-on-error` is set, catch `TeamsPhoneError` and continue; without flag, stop on first error
4. Track results: user, policy_type, policy_name, status ('success'/'error'), error_message
5. Display summary after completion with success/fail counts
6. Write results to CSV file when `--output` specified (or JSON if `.json` extension)
7. Handle KeyboardInterrupt for graceful cancellation

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 31.1 | CSV parsing and validation | Validation phase complete; placeholder code at lines 691-751 needs replacement |
| 24 | Numbers bulk-assign | Full execution pattern with progress bar - key reference at `numbers.py:1336-1548` |

### Reusable Patterns from Dependencies
- **Execution loop pattern**: See `numbers.py:1360-1474` for the complete pattern with progress bar updates
- **Results tracking pattern**: `BulkPolicyResult` dataclass already exists at `bulk_operations.py:118-136`
- **Results file writing**: `write_policy_results_csv()` already implemented at `bulk_operations.py:330-388`
- **Progress bar context**: `formatter.progress(total, description)` at `output_formatter.py:191-227`

### Warnings and Gotchas
- **TeamsVoicemailPolicy**: Already rejected in validation phase (subtask 31.1)
- **Policy assignment is synchronous**: Unlike number assignment which returns 202 and needs polling, policy assignment via `assign_policy()` is immediate (no polling needed)
- **`_resolve_policy_type()` behavior**: Returns unknown types as-is, but validation already handles this
- **Circular import in tests**: Import through `teams_phone.cli.main` not directly from services

### Open Items Inherited
- [x] Implement execution phase with user resolution via `UserService.resolve_user()` - THIS SUBTASK
- [x] Add progress bar using `formatter.progress()` context manager - THIS SUBTASK
- [x] Call `PolicyService.assign_policy()` for each valid row - THIS SUBTASK
- [x] Track success/fail/skipped counts during execution - THIS SUBTASK
- [x] Write results CSV using `write_policy_results_csv()` - THIS SUBTASK
- [x] Handle KeyboardInterrupt for graceful cancellation - THIS SUBTASK
- [x] Display execution summary with counts - THIS SUBTASK

---

## Architecture Context

### Relevant Design Decisions
- **Repository pattern**: Services abstract Graph API access; `PolicyService.assign_policy()` handles policy ID resolution internally
- **Synchronous policy assignment**: Unlike phone number operations (202 + polling), policy assignment completes immediately
- **`_run_async()` pattern**: Creates GraphClient context and runs async operations; already used for single `assign` command at line 433

### Module Structure
```
src/teams_phone/
  cli/
    policies.py      # <-- MODIFY: Add execution loop at lines 691-751
  services/
    bulk_operations.py   # BulkPolicyResult, write_policy_results_csv already exist
    policy_service.py    # assign_policy() already implemented
    user_service.py      # resolve_user() for user resolution
  infrastructure/
    output_formatter.py  # progress() context manager
```

### Conventions to Follow
- Use `_run_async()` helper at `policies.py:160-190` for async context creation
- Follow progress bar pattern from `numbers.py:1360-1474`
- Results file always written (with timestamp if `--output` not specified)
- JSON output mode uses `formatter.json()` for structured output

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/numbers.py` | 1336-1548 | **Reference implementation** - bulk-assign execution with progress, results, error handling |
| `src/teams_phone/cli/policies.py` | 691-751 | **Placeholder to replace** - current "pending implementation" stub |
| `src/teams_phone/cli/policies.py` | 160-190 | `_run_async()` helper for creating async context with services |
| `src/teams_phone/cli/policies.py` | 427-433 | Single `assign` command showing `policy_service.assign_policy()` usage |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/output_formatter.py` | 191-227 | `progress()` context manager implementation |
| `src/teams_phone/services/bulk_operations.py` | 118-136 | `BulkPolicyResult` dataclass for tracking results |
| `src/teams_phone/services/bulk_operations.py` | 330-388 | `write_policy_results_csv()` for output file |
| `src/teams_phone/services/policy_service.py` | 273-336 | `assign_policy()` implementation |
| `src/teams_phone/services/user_service.py` | 191-265 | `resolve_user()` implementation |

### Code Patterns to Follow

```python
# Pattern from numbers.py:1360-1474 - Execution with progress bar
total = len(valid_rows)
with formatter.progress(total, "Processing") as progress:
    task_id = list(progress.task_ids)[0]

    async def _execute_with_progress(
        bulk_ops: BulkOperations, config_manager: ConfigManager
    ) -> list[BulkAssignResult]:
        nonlocal success_count, fail_count
        execution_results: list[BulkAssignResult] = []

        for idx, row in enumerate(valid_rows, start=1):
            # Update progress description with current counts
            desc = f"Processing [{idx}/{total}] OK:{success_count} FAIL:{fail_count}"
            progress.update(task_id, description=desc)

            try:
                # ... execute operation ...
                success_count += 1
                execution_results.append(BulkAssignResult(..., status="success"))
            except TeamsPhoneError as e:
                fail_count += 1
                execution_results.append(BulkAssignResult(..., status="failed", error=e.message))
                if not continue_on_error:
                    progress.update(task_id, advance=1)
                    break

            progress.update(task_id, advance=1)

        return execution_results
```

```python
# Pattern for async policy execution (adapted from policies.py assign command)
async def _execute_assignment(
    policy_service: PolicyService, user_service: UserService
) -> None:
    # Resolve user to get user_id
    user_config = await user_service.resolve_user(row.user)
    user_id = user_config.id

    # Assign policy (synchronous - no polling needed)
    await policy_service.assign_policy(user_id, row.policy_type, row.policy_name)
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.policies` - Current module being modified
- `teams_phone.services.bulk_operations` - `BulkPolicyResult`, `write_policy_results_csv`
- `teams_phone.services.policy_service` - `PolicyService.assign_policy()`
- `teams_phone.services.user_service` - `UserService.resolve_user()`
- `teams_phone.infrastructure.output_formatter` - `progress()` context manager
- `teams_phone.exceptions` - `TeamsPhoneError` for error handling

### External Packages
- `rich` - Progress bar via OutputFormatter.progress()
- `typer` - CLI framework (already imported)
- `datetime` - For timestamp in output filename

### New Dependencies Needed
- None required - all dependencies already in place

---

## Testing Guidance

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

### Test Patterns in Use
```python
# Example test structure from test_cli_policies.py:796-829
@pytest.mark.unit
def test_bulk_assign_dry_run_valid_csv(self, tmp_path: Any) -> None:
    """bulk-assign --dry-run shows validation summary for valid CSV."""
    csv_content = """user,policy_type,policy_name
john@contoso.com,calling,AllowCalling"""
    csv_file = tmp_path / "policies.csv"
    csv_file.write_text(csv_content, encoding="utf-8")

    policy = _make_policy(policy_type="TeamsCallingPolicy", policy_name="AllowCalling")

    with patch("teams_phone.cli.policies._get_sync_service") as mock_get_service:
        mock_service = MagicMock()
        mock_service.get_staleness_warning.return_value = None
        mock_service.validate_policy.return_value = policy
        mock_get_service.return_value = mock_service

        result = runner.invoke(
            app,
            ["--no-color", "policies", "bulk-assign", str(csv_file), "--dry-run"],
        )
        assert result.exit_code == 0
```

### Fixtures Available
- `tmp_path` - pytest built-in for temporary files
- `runner` - Typer CliRunner at module level
- `_make_policy()` - Helper to create mock Policy objects

### Test Utilities
- `unittest.mock.patch` - For mocking services
- `unittest.mock.MagicMock` - For creating mock objects
- `unittest.mock.AsyncMock` - For async method mocking

### Tests to Add
1. **test_bulk_assign_execution_success** - Verify successful assignments with progress
2. **test_bulk_assign_execution_json_output** - JSON mode execution output format
3. **test_bulk_assign_results_csv_written** - Verify results file created
4. **test_bulk_assign_continue_on_error_execution** - Continue after failures
5. **test_bulk_assign_stop_on_error_execution** - Stop on first error without flag
6. **test_bulk_assign_keyboard_interrupt** - Graceful cancellation handling
7. **test_bulk_assign_summary_display** - Verify summary counts shown

---

## Risks and Blockers

### Technical Risks
- [ ] **Async context complexity**: Need to create new async helper or adapt `_run_async()` for bulk operations. Mitigation: Follow `_run_bulk_assign_async()` pattern from `numbers.py:133-165`

### Dependency Risks
- None - all dependencies (PolicyService, UserService, write_policy_results_csv) already implemented

### Open Questions
- [ ] Should results file be written by default (with timestamp) like numbers bulk-assign, or only when `--output` specified? **Recommendation**: Follow numbers pattern - always write with timestamp-based name if `--output` not specified

---

## Implementation Recommendations

### Suggested Approach
1. **Create `_run_bulk_policy_async()` helper** - Similar to `_run_bulk_assign_async()` in numbers.py but for policy operations (or reuse `_run_async()` which already provides PolicyService and UserService)
2. **Replace placeholder code at lines 691-751** with execution loop following numbers.py pattern
3. **Track results** - Initialize results list, add skipped rows, then execute valid rows with progress
4. **Handle errors** - Catch `TeamsPhoneError`, record failures, respect `--continue-on-error`
5. **Write results file** - Use `write_policy_results_csv()` with timestamp-based name if no `--output`
6. **Display summary** - Success/fail/skipped counts, results file path
7. **Add tests** - Mock `_run_async()` and verify execution behavior

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/cli/policies.py` | Replace placeholder execution code (lines 691-751) |
| Modify | `tests/unit/test_cli_policies.py` | Add execution tests for bulk-assign |

### Quick Wins
- Import `datetime` at top of policies.py (already imported in numbers.py)
- The `write_policy_results_csv()` function is already implemented and tested
- `BulkPolicyResult` dataclass already matches the expected output format

---

## Cheat Sheet

### Key Imports
```python
from datetime import datetime
from teams_phone.services.bulk_operations import (
    BulkPolicyResult,
    write_policy_results_csv,
)
```

### Key Functions to Use
- `_run_async(ctx, coro_factory)` - Creates GraphClient context with PolicyService and UserService (policies.py:160-190)
- `user_service.resolve_user(user)` - Returns UserConfiguration with `.id` property
- `policy_service.assign_policy(user_id, policy_type, policy_name)` - Assigns policy (synchronous, no polling)
- `formatter.progress(total, description)` - Context manager for Rich progress bar
- `write_policy_results_csv(results, output_path)` - Writes results to CSV file

### Common Patterns
- **Progress bar update**: `progress.update(task_id, description=desc, advance=1)` (see numbers.py:1376-1452)
- **Results tracking**: Append `BulkPolicyResult` to list with status "success"/"failed"/"skipped"
- **Error handling**: Catch `TeamsPhoneError`, record in results, check `continue_on_error` flag
- **Output path**: `output_path = Path(f"bulk-assign-results-{timestamp}.csv")` if no `--output`

### Key Differences from Numbers Bulk-Assign
1. **No polling needed**: Policy assignment is synchronous (no 202 response + polling)
2. **No number type lookup**: Don't need to call `get_number()` to determine number_type
3. **No location handling**: Policies don't have emergency location requirements
4. **Simpler execution**: Just resolve user, call assign_policy, record result

---

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