create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); DB::table('policies')->insert([ [ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => null, 'external_id' => 'legacy-policy-a', 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows', 'display_name' => 'Legacy Policy A', 'metadata' => json_encode([], JSON_THROW_ON_ERROR), 'last_synced_at' => now(), 'created_at' => now(), 'updated_at' => now(), ], [ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => null, 'external_id' => 'legacy-policy-b', 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows', 'display_name' => 'Legacy Policy B', 'metadata' => json_encode([], JSON_THROW_ON_ERROR), 'last_synced_at' => now(), 'created_at' => now(), 'updated_at' => now(), ], ]); $this->artisan('tenantpilot:backfill-workspace-ids', [ '--table' => 'policies', '--batch-size' => 1, ])->assertSuccessful(); $missingAfter = DB::table('policies') ->where('tenant_id', (int) $tenant->getKey()) ->whereNull('workspace_id') ->count(); expect($missingAfter)->toBe(0); $workspaceIds = DB::table('policies') ->where('tenant_id', (int) $tenant->getKey()) ->pluck('workspace_id') ->map(static fn (mixed $workspaceId): int => (int) $workspaceId) ->unique() ->values() ->all(); expect($workspaceIds)->toBe([(int) $workspace->getKey()]); $run = OperationRun::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('type', 'workspace_isolation_backfill_workspace_ids') ->latest('id') ->first(); expect($run)->not->toBeNull(); expect((int) data_get($run?->summary_counts, 'processed', 0))->toBe(2); $actions = AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->pluck('action') ->all(); expect($actions)->toContain('workspace_isolation.backfill_workspace_ids.started'); expect($actions)->toContain('workspace_isolation.backfill_workspace_ids.dispatched'); }); it('is idempotent when re-run after backfill', function (): void { $workspace = Workspace::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); DB::table('policies')->insert([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => null, 'external_id' => 'legacy-policy-retry', 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows', 'display_name' => 'Legacy Retry Policy', 'metadata' => json_encode([], JSON_THROW_ON_ERROR), 'last_synced_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $this->artisan('tenantpilot:backfill-workspace-ids', ['--table' => 'policies'])->assertSuccessful(); $this->artisan('tenantpilot:backfill-workspace-ids', ['--table' => 'policies']) ->expectsOutputToContain('No rows require workspace_id backfill.') ->assertSuccessful(); $missingAfter = DB::table('policies') ->where('tenant_id', (int) $tenant->getKey()) ->whereNull('workspace_id') ->count(); expect($missingAfter)->toBe(0); }); it('aborts and reports when tenant to workspace mapping is unresolvable', function (): void { $tenant = Tenant::factory()->create(); $tenant->forceFill(['workspace_id' => null])->save(); DB::table('policies')->insert([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => null, 'external_id' => 'legacy-policy-unresolvable', 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows', 'display_name' => 'Legacy Unresolvable Policy', 'metadata' => json_encode([], JSON_THROW_ON_ERROR), 'last_synced_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $this->artisan('tenantpilot:backfill-workspace-ids', ['--table' => 'policies']) ->expectsOutputToContain('Unresolvable tenant->workspace mapping') ->assertFailed(); $missingAfter = DB::table('policies') ->where('tenant_id', (int) $tenant->getKey()) ->whereNull('workspace_id') ->count(); expect($missingAfter)->toBe(1); });