--- 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