90, 'ai-editing.retention.usage_ledger_days' => 365, ]); [$oldRequest, $recentRequest] = $this->createRequestsForPruning(); [$oldLedger, $recentLedger] = $this->createLedgerEntriesForPruning($oldRequest, $recentRequest); $this->artisan('ai-edits:prune') ->expectsOutputToContain('AI prune candidates') ->expectsOutputToContain('Pruned AI data') ->assertExitCode(0); $this->assertDatabaseMissing('ai_edit_requests', ['id' => $oldRequest->id]); $this->assertDatabaseMissing('ai_provider_runs', ['request_id' => $oldRequest->id]); $this->assertDatabaseMissing('ai_edit_outputs', ['request_id' => $oldRequest->id]); $this->assertDatabaseHas('ai_edit_requests', ['id' => $recentRequest->id]); $this->assertDatabaseMissing('ai_usage_ledgers', ['id' => $oldLedger->id]); $this->assertNotNull($recentLedger); if ($recentLedger) { $this->assertDatabaseHas('ai_usage_ledgers', ['id' => $recentLedger->id]); } } public function test_command_pretend_mode_does_not_delete_records(): void { config([ 'ai-editing.retention.request_days' => 90, 'ai-editing.retention.usage_ledger_days' => 365, ]); [$oldRequest] = $this->createRequestsForPruning(); [$oldLedger] = $this->createLedgerEntriesForPruning($oldRequest, null); $this->artisan('ai-edits:prune', ['--pretend' => true]) ->expectsOutputToContain('AI prune candidates') ->expectsOutput('Pretend mode enabled. No records were deleted.') ->assertExitCode(0); $this->assertDatabaseHas('ai_edit_requests', ['id' => $oldRequest->id]); $this->assertDatabaseHas('ai_usage_ledgers', ['id' => $oldLedger->id]); } /** * @return array{0: AiEditRequest, 1: AiEditRequest} */ private function createRequestsForPruning(): array { $event = Event::factory()->create(['status' => 'published']); $photo = Photo::factory()->for($event)->create([ 'tenant_id' => $event->tenant_id, 'status' => 'approved', ]); $style = AiStyle::query()->create([ 'key' => 'prune-style', 'name' => 'Prune Style', 'provider' => 'runware', 'provider_model' => 'runware-default', 'requires_source_image' => true, 'is_active' => true, ]); $oldRequest = AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'style_id' => $style->id, 'provider' => 'runware', 'provider_model' => 'runware-default', 'status' => AiEditRequest::STATUS_SUCCEEDED, 'safety_state' => 'passed', 'prompt' => 'Old request', 'idempotency_key' => 'old-prune-request', 'queued_at' => now()->subDays(121), 'started_at' => now()->subDays(120), 'completed_at' => now()->subDays(120), 'expires_at' => now()->subDays(30), ]); AiProviderRun::query()->create([ 'request_id' => $oldRequest->id, 'provider' => 'runware', 'attempt' => 1, 'provider_task_id' => 'old-task-id', 'status' => AiProviderRun::STATUS_SUCCEEDED, 'started_at' => now()->subDays(120), 'finished_at' => now()->subDays(120), ]); AiEditOutput::query()->create([ 'request_id' => $oldRequest->id, 'provider_asset_id' => 'old-asset-id', 'provider_url' => 'https://cdn.example.invalid/old.jpg', 'safety_state' => 'passed', 'generated_at' => now()->subDays(120), ]); $recentRequest = AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'style_id' => $style->id, 'provider' => 'runware', 'provider_model' => 'runware-default', 'status' => AiEditRequest::STATUS_SUCCEEDED, 'safety_state' => 'passed', 'prompt' => 'Recent request', 'idempotency_key' => 'recent-prune-request', 'queued_at' => now()->subDays(11), 'started_at' => now()->subDays(10), 'completed_at' => now()->subDays(10), ]); return [$oldRequest, $recentRequest]; } /** * @return array{0: AiUsageLedger, 1: ?AiUsageLedger} */ private function createLedgerEntriesForPruning(AiEditRequest $oldRequest, ?AiEditRequest $recentRequest): array { $oldLedger = AiUsageLedger::query()->create([ 'tenant_id' => $oldRequest->tenant_id, 'event_id' => $oldRequest->event_id, 'request_id' => $oldRequest->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 0.01, 'amount_usd' => 0.01, 'currency' => 'USD', 'recorded_at' => now()->subDays(400), ]); if (! $recentRequest) { return [$oldLedger, null]; } $recentLedger = AiUsageLedger::query()->create([ 'tenant_id' => $recentRequest->tenant_id, 'event_id' => $recentRequest->event_id, 'request_id' => $recentRequest->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 0.01, 'amount_usd' => 0.01, 'currency' => 'USD', 'recorded_at' => now()->subDays(30), ]); return [$oldLedger, $recentLedger]; } }