---

description: "Task list for implementing Async “Add Policies” to Backup Set (052)"
---

# Tasks: Async “Add Policies” to Backup Set (052)

**Input**: Design documents from `/specs/052-async-add-policies/`
**Prerequisites**: plan.md (required), spec.md (required)

**Tests**: Required (Pest), per spec.md (SC-001..SC-004).

**Organization**: Tasks are grouped by user story so each story can be implemented and tested independently.

## Format: `- [ ] T### [P?] [US#] Description (with file path)`

- **[P]**: Can run in parallel (different files, no dependencies)
- **[US#]**: User story mapping (US1/US2/US3)

## Path Conventions (Laravel)

- App code: `app/`
- DB: `database/migrations/`
- Filament admin: `app/Filament/Resources/`
- Livewire tables: `app/Livewire/`
- Tests (Pest): `tests/Feature/`, `tests/Unit/`

---

## Phase 1: Setup (Shared Infrastructure)

- [x] T001 [P] Confirm existing run infra supports this operation (BulkOperationRun statuses + unique idempotency index) and document mapping queued↔pending, partial↔completed_with_errors in specs/052-async-add-policies/plan.md
- [x] T002 [P] Decide and standardize taxonomy strings: operationType (`backup_set.add_policies`), resource (`backup_set`), action (`add_policies`) in specs/052-async-add-policies/spec.md and plan.md

---

## Phase 2: User Story 1 - Add selected policies without blocking (Priority: P1) 🎯 MVP

**Goal**: “Add selected” returns quickly and queues background work with an observable Run record.

**Independent Test**: Trigger the action and assert a job is queued and a run exists; no Graph/capture work occurs in-request.

### Tests (write first) ⚠️

- [x] T010 [P] [US1] Update/replace sync-path assertions in tests/Feature/Filament/BackupSetPolicyPickerTableTest.php to assert job dispatch + run creation
- [x] T011 [P] [US1] Add fail-hard guard test to ensure no Graph calls occur during the bulk action (mock `App\\Services\\Graph\\GraphClientInterface` and assert `->never()`)

### Implementation

- [x] T020 [US1] Create queued job to add policies to a backup set in app/Jobs/AddPoliciesToBackupSetJob.php (uses run lifecycle + safe failures via BulkOperationService)
- [x] T021 [US1] Update bulk action handler in app/Livewire/BackupSetPolicyPickerTable.php to create/reuse BulkOperationRun and dispatch AddPoliciesToBackupSetJob (no inline snapshot capture)
- [x] T022 [US1] Emit “queued” notification with “View run” link on submit (Filament DB notification preferred) from app/Livewire/BackupSetPolicyPickerTable.php
- [x] T023 [US1] Emit completion/failure DB notification from app/Jobs/AddPoliciesToBackupSetJob.php with a safe summary

**Checkpoint**: US1 complete — submit is non-blocking, job executes, run is visible.

---

## Phase 3: User Story 2 - Double click / repeated submissions are deduplicated (Priority: P2)

**Goal**: Matching queued/running operations reuse the same run and do not enqueue duplicates.

**Independent Test**: Call the action twice for the same tenant + backup set + selection and assert only one run and one job dispatch.

### Tests ⚠️

- [x] T030 [P] [US2] Add idempotency test in tests/Feature/BackupSets/BackupSetAddPoliciesIdempotencyTest.php (or extend existing picker test) asserting one run + one queued job

### Implementation

- [x] T031 [US2] Implement deterministic selection hashing and idempotency key creation using app/Support/RunIdempotency.php (sorted policy ids + option flags), and reuse active runs via findActiveBulkOperationRun
- [x] T032 [US2] Handle race conditions safely (unique index collisions) by recovering the existing run rather than failing the request
- [x] T033 [US2] Ensure the action is always async (no `dispatchSync` path for small selections) in app/Livewire/BackupSetPolicyPickerTable.php

**Checkpoint**: US2 complete — double clicks are safe and deduped.

---

## Phase 4: User Story 3 - Failures are visible and safe (Priority: P3)

**Goal**: Failures are persisted safely and tenant isolation is enforced for run visibility.

**Independent Test**: Force failure paths and confirm safe failures persisted; cross-tenant access is forbidden.

### Tests ⚠️

- [x] T040 [P] [US3] Add tenant isolation test for the created run (403 cross-tenant) in tests/Feature/BackupSets/BackupSetAddPoliciesTenantIsolationTest.php (or extend tests/Feature/RunAuthorizationTenantIsolationTest.php)
- [x] T041 [P] [US3] Add sanitization test: failure reason containing token-like content is stored as redacted (exercise BulkOperationService::sanitizeFailureReason)

### Implementation

- [x] T042 [US3] Ensure job records per-item failures with sanitized reasons and does not store raw Graph payloads in run failures or notifications (app/Jobs/AddPoliciesToBackupSetJob.php)
- [x] T043 [US3] Record stable failure `reason_code` values (per spec.md) alongside sanitized short text in run failures (app/Jobs/AddPoliciesToBackupSetJob.php and/or app/Services/BulkOperationService.php)
- [x] T044 [US3] Record “already in backup set” as `skipped` (with reason_code `already_in_backup_set`) and ensure counts match spec.md (app/Jobs/AddPoliciesToBackupSetJob.php)
- [x] T045 [US3] Ensure job processes all items (no circuit breaker abort) and run status reflects partial completion (app/Jobs/AddPoliciesToBackupSetJob.php)

**Checkpoint**: US3 complete — failures are safe and observable; tenant isolation holds.

---

## Phase 5: Polish & Validation

- [x] T050 [P] Run formatting on changed files with ./vendor/bin/pint --dirty
- [x] T051 Run targeted tests: ./vendor/bin/sail artisan test --filter=BackupSetAddPolicies
