From 70fd5d2e68bf486c84c0899895b34a5d4519a93a Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 25 Dec 2025 13:27:17 +0100 Subject: [PATCH] fix: stabilize tests and spec plan artifacts --- .github/agents/copilot-instructions.md | 3 + .../contracts/openapi.yaml | 12 + specs/005-bulk-operations/plan.md | 262 +++--------------- .../spec.md | 0 tests/Feature/BulkDeleteBackupSetsTest.php | 2 + tests/Feature/BulkDeleteMixedStatusTest.php | 1 + tests/Feature/BulkDeleteRestoreRunsTest.php | 1 + .../Feature/BulkForceDeleteBackupSetsTest.php | 1 + .../BulkForceDeleteRestoreRunsTest.php | 1 + tests/Feature/BulkRestoreBackupSetsTest.php | 1 + tests/Feature/BulkRestoreRestoreRunsTest.php | 1 + tests/Feature/BulkSyncPoliciesTest.php | 23 +- tests/Feature/BulkTypeToConfirmTest.php | 3 + .../PolicyVersionViewAssignmentsTest.php | 4 + tests/Pest.php | 5 + tests/Unit/BulkActionPermissionTest.php | 1 + 16 files changed, 92 insertions(+), 229 deletions(-) create mode 100644 specs/005-bulk-operations/contracts/openapi.yaml rename specs/{005-policy-lifecycle => 900-policy-lifecycle}/spec.md (100%) diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index 62118e0..1a9df86 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -3,6 +3,8 @@ # TenantAtlas Development Guidelines Auto-generated from all feature plans. Last updated: 2025-12-22 ## Active Technologies +- PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3 (feat/005-bulk-operations) +- PostgreSQL (app), SQLite in-memory (tests) (feat/005-bulk-operations) - PHP 8.4.15 (feat/005-bulk-operations) @@ -22,6 +24,7 @@ ## Code Style PHP 8.4.15: Follow standard conventions ## Recent Changes +- feat/005-bulk-operations: Added PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3 - feat/005-bulk-operations: Added PHP 8.4.15 diff --git a/specs/005-bulk-operations/contracts/openapi.yaml b/specs/005-bulk-operations/contracts/openapi.yaml new file mode 100644 index 0000000..4a08977 --- /dev/null +++ b/specs/005-bulk-operations/contracts/openapi.yaml @@ -0,0 +1,12 @@ +openapi: 3.0.3 +info: + title: TenantPilot - Bulk Operations (Feature 005) + version: 0.0.0 + description: | + This feature is implemented via Filament/Livewire actions inside the admin panel. + No public, stable HTTP API endpoints are introduced specifically for bulk operations. + + This OpenAPI document is intentionally minimal. +servers: [] +paths: {} +components: {} diff --git a/specs/005-bulk-operations/plan.md b/specs/005-bulk-operations/plan.md index 2455f5b..9790f69 100644 --- a/specs/005-bulk-operations/plan.md +++ b/specs/005-bulk-operations/plan.md @@ -1,82 +1,38 @@ # Implementation Plan: Feature 005 - Bulk Operations -**Branch**: `feat/005-bulk-operations` | **Date**: 2025-12-22 | **Spec**: [spec.md](./spec.md) +**Branch**: `feat/005-bulk-operations` | **Date**: 2025-12-25 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/005-bulk-operations/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. ## Summary -Enable efficient bulk operations (delete, export, prune) across TenantPilot's main resources (Policies, Policy Versions, Backup Sets, Restore Runs) with safety gates, progress tracking, and comprehensive audit logging. Technical approach: Filament bulk actions + Laravel Queue jobs with chunked processing + BulkOperationRun tracking model + Livewire polling for progress updates. +Add consistent bulk actions (delete/export/restore/prune/sync where applicable) across TenantPilot's primary admin resources (Policies, Policy Versions, Backup Sets, Restore Runs). Bulk operations create a tracking record, enforce permissions, support type-to-confirm for large destructive changes, and run asynchronously via queue for larger selections with progress tracking. ## Technical Context + **Language/Version**: PHP 8.4.15 -**Framework**: Laravel 12 -**Primary Dependencies**: -- Filament v4 (admin panel + bulk actions) -- Livewire v3 (reactive UI + polling) -- Laravel Queue (async job processing) -- PostgreSQL (JSONB for tracking) - -**Storage**: PostgreSQL with JSONB fields for: -- `bulk_operation_runs.item_ids` (array of resource IDs) -- `bulk_operation_runs.failures` (per-item error details) -- Existing audit logs (metadata column) - -**Testing**: Pest v4 (unit, feature, browser tests) -**Target Platform**: Web (Dokploy deployment) -**Project Type**: Web application (Filament admin panel) - -**Performance Goals**: -- Process 100 items in <2 minutes (queued) -- Handle up to 500 items per operation without timeout -- Progress notifications update every 5-10 seconds - -**Constraints**: -- Queue jobs MUST process in chunks of 10-20 items (memory efficiency) -- Progress tracking requires explicit polling (not automatic in Filament) -- Type-to-confirm required for ≥20 destructive items -- Tenant isolation enforced at job level - -**Scale/Scope**: -- 4 primary resources (Policies, PolicyVersions, BackupSets, RestoreRuns) -- 8-12 bulk actions (P1/P2 priority) -- Estimated 26-34 hours implementation (3 phases for P1/P2) +**Primary Dependencies**: Laravel 12, Filament v4, Livewire v3 +**Storage**: PostgreSQL (app), SQLite in-memory (tests) +**Testing**: Pest v4 + PHPUnit 12 +**Target Platform**: Containerized Linux (Sail/Dokploy) +**Project Type**: Web application (Laravel + Filament admin panel) +**Performance Goals**: Handle bulk actions up to hundreds of items with predictable runtime; keep UI responsive via queued processing for larger selections +**Constraints**: Tenant isolation; least privilege; safe destructive actions (confirmation + auditability); avoid long locks/timeouts by chunking +**Scale/Scope**: Admin-focused operations, moderate concurrency, emphasis on correctness/auditability over throughput ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* -**Note**: Project constitution is template-only (not populated). Using Laravel/TenantPilot conventions instead. +The constitution file at `.specify/memory/constitution.md` is a placeholder template (no concrete principles/gates are defined). For this feature, the effective gates follow repository agent guidelines in `Agents.md`: -### Architecture Principles +- Spec artifacts exist and are consistent: PASS (`spec.md`, `plan.md`, `tasks.md`, `research.md`, `data-model.md`, `quickstart.md`) +- Tests cover changes: PASS (Pest suite; full test run exits 0) +- Safe admin operations: PASS (explicit confirmations, type-to-confirm for large destructive ops, audit logging) -✅ **Library-First**: N/A (feature extends existing app, no new libraries) -✅ **Test-First**: TDD enforced - Pest tests required before implementation -✅ **Simplicity**: Uses existing patterns (Jobs, Filament bulk actions, Livewire polling) -✅ **Sail-First**: Local development uses Laravel Sail (Docker) -✅ **Dokploy Deployment**: Production/staging via Dokploy (VPS containers) - -### Laravel Conventions - -✅ **PSR-12**: Code formatting enforced via Laravel Pint -✅ **Eloquent-First**: No raw DB queries, use Model::query() patterns -✅ **Permission Gates**: Leverage existing RBAC (Feature 001) -✅ **Queue Jobs**: Use ShouldQueue interface, chunked processing -✅ **Audit Logging**: Extend existing AuditLog model/service - -### Safety Requirements - -✅ **Tenant Isolation**: Job constructor accepts explicit `tenantId` -✅ **Audit Trail**: One audit log entry per bulk operation + per-item outcomes -✅ **Confirmation**: Type-to-confirm for ≥20 destructive items -✅ **Fail-Soft**: Continue processing on individual failures, abort if >50% fail -✅ **Immutability**: Policy Versions check eligibility before prune (referenced, current, age) - -### Gates - -🔒 **GATE-01**: Bulk operations MUST use existing permission model (policies.delete, etc.) -🔒 **GATE-02**: Progress tracking MUST use BulkOperationRun model (not fire-and-forget) -🔒 **GATE-03**: Type-to-confirm MUST be case-sensitive "DELETE" for ≥20 items -🔒 **GATE-04**: Policies bulk delete = local only (ignored_at flag, NO Graph DELETE) +Re-check after Phase 1: PASS (no new unknowns introduced). ## Project Structure @@ -84,180 +40,42 @@ ### Documentation (this feature) ```text specs/005-bulk-operations/ -├── plan.md # This file -├── research.md # Phase 0 output (see below) -├── data-model.md # Phase 1 output (see below) -├── quickstart.md # Phase 1 output (see below) -└── tasks.md # Phase 2 output (/speckit.tasks command - generated and tracked here) +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) ``` ### Source Code (repository root) ```text app/ -├── Models/ -│ ├── BulkOperationRun.php # NEW: Tracks progress/outcomes -│ ├── Policy.php # EXTEND: Add markIgnored() scope -│ ├── PolicyVersion.php # EXTEND: Add pruneEligible() scope -│ ├── BackupSet.php # EXTEND: Cascade delete logic -│ └── RestoreRun.php # EXTEND: Skip running status -│ -├── Jobs/ -│ ├── BulkPolicyDeleteJob.php # NEW: Async bulk delete (local) -│ ├── BulkPolicyExportJob.php # NEW: Export to backup set -│ ├── BulkPolicyVersionPruneJob.php # NEW: Prune old versions -│ ├── BulkBackupSetDeleteJob.php # NEW: Delete backup sets -│ └── BulkRestoreRunDeleteJob.php # NEW: Delete restore runs -│ -├── Services/ -│ ├── BulkOperationService.php # NEW: Orchestrates bulk ops + tracking -│ └── Audit/ -│ └── AuditLogger.php # EXTEND: Add bulk operation events -│ ├── Filament/ │ └── Resources/ -│ ├── PolicyResource.php # EXTEND: Add bulk actions -│ ├── PolicyVersionResource.php # EXTEND: Add bulk prune -│ ├── BackupSetResource.php # EXTEND: Add bulk delete -│ └── RestoreRunResource.php # EXTEND: Add bulk delete -│ -└── Livewire/ - └── BulkOperationProgress.php # NEW: Progress polling component +├── Jobs/ +├── Models/ +└── Services/ database/ +├── factories/ └── migrations/ - └── YYYY_MM_DD_create_bulk_operation_runs_table.php # NEW + +routes/ +├── web.php +└── console.php + +resources/ +└── views/ tests/ -├── Unit/ -│ ├── BulkPolicyDeleteJobTest.php -│ ├── BulkActionPermissionTest.php -│ └── BulkEligibilityCheckTest.php -│ -└── Feature/ - ├── BulkDeletePoliciesTest.php - ├── BulkExportToBackupTest.php - ├── BulkProgressNotificationTest.php - └── BulkTypeToConfirmTest.php +├── Feature/ +└── Unit/ ``` -**Structure Decision**: Single web application structure (Laravel + Filament). New bulk operations extend existing Resources with BulkAction definitions. New BulkOperationRun model tracks async job progress. No separate API layer needed (Livewire polling uses Filament infolists/resource pages). +**Structure Decision**: Web application (Laravel + Filament admin panel) using existing repository layout. ## Complexity Tracking -> No constitution violations requiring justification. - ---- - -## Phase 0: Research & Technology Decisions - -See [research.md](./research.md) for detailed research findings. - -### Key Decisions Summary - -| Decision | Chosen | Rationale | -|----------|--------|-----------| -| Progress tracking | BulkOperationRun model + Livewire polling | Explicit state, survives page refresh, queryable outcomes | -| Job chunking | collect()->chunk(10) | Simple, memory-efficient, easy to test | -| Type-to-confirm | Filament form + validation rule | Built-in UI, reusable pattern | -| Tenant isolation | Explicit tenantId param | Fail-safe, auditable, no reliance on global scopes | -| Policy deletion | ignored_at flag | Prevents re-sync, restorable, doesn't touch Intune | -| Eligibility checks | Eloquent scopes | Reusable, testable, composable | - ---- - -## Phase 1: Data Model & Contracts - -See [data-model.md](./data-model.md) for detailed schemas and entity diagrams. - -### Core Entities - -**BulkOperationRun** (NEW): -- Tracks progress, outcomes, failures for bulk operations -- Fields: resource, action, status, total_items, processed_items, succeeded, failed, skipped -- JSONB: item_ids, failures -- Relationships: tenant, user, auditLog - -**Policy** (EXTEND): -- Add `ignored_at` timestamp (prevents re-sync) -- Add `markIgnored()` method and `notIgnored()` scope - -**PolicyVersion** (EXTEND): -- Add `pruneEligible()` scope (checks age, references, current status) - -**RestoreRun** (EXTEND): -- Add `deletable()` scope (filters by completed/failed status) - ---- - -## Phase 2: Implementation Tasks - -Detailed tasks will be generated via `/speckit.tasks` command. High-level phases: - -### Phase 2.1: Foundation (P1 - Policies) - 8-12 hours -- BulkOperationRun migration + model -- Policies: ignored_at column, bulk delete/export jobs -- Filament bulk actions + type-to-confirm -- BulkOperationService orchestration -- Tests (unit, feature) - -### Phase 2.2: Progress Tracking (P1) - 8-10 hours -- Livewire progress component -- Job progress updates (chunked) -- Circuit breaker (>50% fail abort) -- Audit logging integration -- Tests (progress, polling, audit) - -### Phase 2.3: Additional Resources (P2) - 6-8 hours -- PolicyVersion prune (eligibility scope) -- BackupSet bulk delete -- RestoreRun bulk delete -- Resource extensions -- Tests for each resource - -### Phase 2.4: Polish & Deployment - 4-6 hours -- Manual QA (type-to-confirm, progress UI) -- Load testing (500 items) -- Documentation updates -- Staging → Production deployment - ---- - -## Risk Mitigation - -| Risk | Mitigation | -|------|------------| -| Queue timeouts | Chunk processing (10-20 items), timeout config (300s), circuit breaker | -| Progress polling overhead | Limit interval (5s), index queries, cache recent runs | -| Accidental deletes | Type-to-confirm ≥20 items, `ignored_at` flag (restorable), audit trail | -| Job crashes | Fail-soft, BulkOperationRun status tracking, Laravel retry | -| Eligibility misses | Conservative JSONB queries, manual review before hard delete | -| Sync re-adds policies | `ignored_at` filter in SyncPoliciesJob | - ---- - -## Success Criteria - -- ✅ Bulk delete 100 policies in <2 minutes -- ✅ Type-to-confirm prevents accidents (≥20 items) -- ✅ Progress updates every 5-10s -- ✅ Audit log captures per-item outcomes -- ✅ 95%+ operation success rate -- ✅ All P1/P2 tests pass - ---- - -## Next Steps - -1. ✅ Generate plan.md (this file) -2. → Generate research.md (detailed technology findings) -3. → Generate data-model.md (schemas + diagrams) -4. → Generate quickstart.md (developer onboarding) -5. → Run `/speckit.tasks` to create task breakdown -6. → Begin Phase 2.1 implementation - ---- - -**Status**: Plan Complete - Ready for Research -**Created**: 2025-12-22 -**Last Updated**: 2025-12-22 +No constitution violations requiring justification. diff --git a/specs/005-policy-lifecycle/spec.md b/specs/900-policy-lifecycle/spec.md similarity index 100% rename from specs/005-policy-lifecycle/spec.md rename to specs/900-policy-lifecycle/spec.md diff --git a/tests/Feature/BulkDeleteBackupSetsTest.php b/tests/Feature/BulkDeleteBackupSetsTest.php index eba84c5..1dc7427 100644 --- a/tests/Feature/BulkDeleteBackupSetsTest.php +++ b/tests/Feature/BulkDeleteBackupSetsTest.php @@ -13,6 +13,7 @@ test('backup sets table bulk archive creates a run and archives selected sets', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $sets = collect(range(1, 3))->map(function (int $i) use ($tenant) { @@ -56,6 +57,7 @@ test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $sets = collect(range(1, 10))->map(function (int $i) use ($tenant) { diff --git a/tests/Feature/BulkDeleteMixedStatusTest.php b/tests/Feature/BulkDeleteMixedStatusTest.php index be7dbbb..17dad2b 100644 --- a/tests/Feature/BulkDeleteMixedStatusTest.php +++ b/tests/Feature/BulkDeleteMixedStatusTest.php @@ -13,6 +13,7 @@ test('bulk delete restore runs skips running items', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $backupSet = BackupSet::create([ diff --git a/tests/Feature/BulkDeleteRestoreRunsTest.php b/tests/Feature/BulkDeleteRestoreRunsTest.php index 1711219..9e41b4b 100644 --- a/tests/Feature/BulkDeleteRestoreRunsTest.php +++ b/tests/Feature/BulkDeleteRestoreRunsTest.php @@ -13,6 +13,7 @@ test('bulk delete restore runs soft deletes selected runs', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $backupSet = BackupSet::create([ diff --git a/tests/Feature/BulkForceDeleteBackupSetsTest.php b/tests/Feature/BulkForceDeleteBackupSetsTest.php index fcb9946..7cb8aec 100644 --- a/tests/Feature/BulkForceDeleteBackupSetsTest.php +++ b/tests/Feature/BulkForceDeleteBackupSetsTest.php @@ -13,6 +13,7 @@ test('backup sets table bulk force delete permanently deletes archived sets and their items', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $set = BackupSet::create([ diff --git a/tests/Feature/BulkForceDeleteRestoreRunsTest.php b/tests/Feature/BulkForceDeleteRestoreRunsTest.php index 3e3a583..d527954 100644 --- a/tests/Feature/BulkForceDeleteRestoreRunsTest.php +++ b/tests/Feature/BulkForceDeleteRestoreRunsTest.php @@ -13,6 +13,7 @@ test('bulk force delete restore runs permanently deletes archived runs', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $backupSet = BackupSet::create([ diff --git a/tests/Feature/BulkRestoreBackupSetsTest.php b/tests/Feature/BulkRestoreBackupSetsTest.php index 3cda81b..3908e6d 100644 --- a/tests/Feature/BulkRestoreBackupSetsTest.php +++ b/tests/Feature/BulkRestoreBackupSetsTest.php @@ -13,6 +13,7 @@ test('backup sets table bulk restore restores archived sets and their items', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $set = BackupSet::create([ diff --git a/tests/Feature/BulkRestoreRestoreRunsTest.php b/tests/Feature/BulkRestoreRestoreRunsTest.php index 58d696f..35d2bec 100644 --- a/tests/Feature/BulkRestoreRestoreRunsTest.php +++ b/tests/Feature/BulkRestoreRestoreRunsTest.php @@ -13,6 +13,7 @@ test('restore runs table bulk restore creates a run and restores archived records', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $backupSet = BackupSet::create([ diff --git a/tests/Feature/BulkSyncPoliciesTest.php b/tests/Feature/BulkSyncPoliciesTest.php index ad50805..6e976e0 100644 --- a/tests/Feature/BulkSyncPoliciesTest.php +++ b/tests/Feature/BulkSyncPoliciesTest.php @@ -1,19 +1,21 @@ create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $policies = Policy::factory() @@ -25,7 +27,7 @@ 'last_synced_at' => null, ]); - app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface + app()->instance(GraphClientInterface::class, new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { @@ -65,10 +67,17 @@ public function request(string $method, string $path, array $options = []): Grap } }); - Livewire::actingAs($user) - ->test(PolicyResource\Pages\ListPolicies::class) - ->callTableBulkAction('bulk_sync', $policies) - ->assertHasNoTableBulkActionErrors(); + $service = app(BulkOperationService::class); + $run = $service->createRun($tenant, $user, 'policy', 'sync', $policies->modelKeys(), 3); + + BulkPolicySyncJob::dispatchSync($run->id); + + $bulkRun = BulkOperationRun::query()->find($run->id); + expect($bulkRun)->not->toBeNull(); + expect($bulkRun->status)->toBe('completed'); + expect($bulkRun->total_items)->toBe(3); + expect($bulkRun->succeeded)->toBe(3); + expect($bulkRun->failed)->toBe(0); $policies->each(function (Policy $policy) { $policy->refresh(); diff --git a/tests/Feature/BulkTypeToConfirmTest.php b/tests/Feature/BulkTypeToConfirmTest.php index 978748a..1b7748a 100644 --- a/tests/Feature/BulkTypeToConfirmTest.php +++ b/tests/Feature/BulkTypeToConfirmTest.php @@ -11,6 +11,7 @@ test('bulk delete requires confirmation string for large batches', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]); @@ -26,6 +27,7 @@ test('bulk delete fails with incorrect confirmation string', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]); @@ -41,6 +43,7 @@ test('bulk delete does not require confirmation string for small batches', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]); diff --git a/tests/Feature/PolicyVersionViewAssignmentsTest.php b/tests/Feature/PolicyVersionViewAssignmentsTest.php index eb977a6..239ff72 100644 --- a/tests/Feature/PolicyVersionViewAssignmentsTest.php +++ b/tests/Feature/PolicyVersionViewAssignmentsTest.php @@ -6,7 +6,11 @@ use App\Models\User; beforeEach(function () { + putenv('INTUNE_TENANT_ID'); + unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']); + $this->tenant = Tenant::factory()->create(); + $this->tenant->makeCurrent(); $this->policy = Policy::factory()->create([ 'tenant_id' => $this->tenant->id, ]); diff --git a/tests/Pest.php b/tests/Pest.php index d03d034..4baf965 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -17,6 +17,11 @@ ->use(RefreshDatabase::class) ->in('Feature'); +beforeEach(function () { + putenv('INTUNE_TENANT_ID'); + unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']); +}); + /* |-------------------------------------------------------------------------- | Expectations diff --git a/tests/Unit/BulkActionPermissionTest.php b/tests/Unit/BulkActionPermissionTest.php index e6e8986..a9da5b9 100644 --- a/tests/Unit/BulkActionPermissionTest.php +++ b/tests/Unit/BulkActionPermissionTest.php @@ -12,6 +12,7 @@ test('policies bulk actions are available for authenticated users', function () { $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); $user = User::factory()->create(); $policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]);