feat(ai): finalize AI magic edits epic rollout and operations
This commit is contained in:
@@ -5,6 +5,7 @@ namespace Tests\Feature\Api\Tenant;
|
||||
use App\Models\AiEditingSetting;
|
||||
use App\Models\AiEditRequest;
|
||||
use App\Models\AiStyle;
|
||||
use App\Models\AiUsageLedger;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackage;
|
||||
use App\Models\EventPackageAddon;
|
||||
@@ -305,6 +306,7 @@ class TenantAiEditControllerTest extends TenantTestCase
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.0.id', $active->id)
|
||||
->assertJsonPath('data.0.version', 1)
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('meta.required_feature', 'ai_styling')
|
||||
->assertJsonPath('meta.event_enabled', false)
|
||||
@@ -540,6 +542,148 @@ class TenantAiEditControllerTest extends TenantTestCase
|
||||
$second->assertStatus(429);
|
||||
}
|
||||
|
||||
public function test_tenant_submit_endpoint_enforces_event_scope_rate_limit(): void
|
||||
{
|
||||
config([
|
||||
'ai-editing.abuse.tenant_submit_per_minute' => 10,
|
||||
'ai-editing.abuse.tenant_submit_per_hour' => 100,
|
||||
'ai-editing.abuse.tenant_submit_per_event_per_minute' => 1,
|
||||
]);
|
||||
|
||||
$event = Event::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'published',
|
||||
]);
|
||||
$this->attachEntitledEventPackage($event);
|
||||
|
||||
$photo = Photo::factory()->for($event)->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
$style = AiStyle::query()->create([
|
||||
'key' => 'tenant-event-rate-limit-style',
|
||||
'name' => 'Tenant Event Rate Limit',
|
||||
'provider' => 'runware',
|
||||
'provider_model' => 'runware-default',
|
||||
'requires_source_image' => true,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$first = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/ai-edits", [
|
||||
'photo_id' => $photo->id,
|
||||
'style_id' => $style->id,
|
||||
'prompt' => 'First request.',
|
||||
'idempotency_key' => 'tenant-event-rate-limit-1',
|
||||
]);
|
||||
$first->assertCreated();
|
||||
|
||||
$second = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/ai-edits", [
|
||||
'photo_id' => $photo->id,
|
||||
'style_id' => $style->id,
|
||||
'prompt' => 'Second request.',
|
||||
'idempotency_key' => 'tenant-event-rate-limit-2',
|
||||
]);
|
||||
$second->assertStatus(429);
|
||||
}
|
||||
|
||||
public function test_tenant_cannot_create_ai_edit_when_hard_budget_cap_is_reached(): void
|
||||
{
|
||||
$event = Event::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'published',
|
||||
]);
|
||||
$this->attachEntitledEventPackage($event);
|
||||
|
||||
$tenantSettings = (array) ($this->tenant->settings ?? []);
|
||||
data_set($tenantSettings, 'ai_editing.budget.hard_cap_usd', 0.01);
|
||||
$this->tenant->update(['settings' => $tenantSettings]);
|
||||
|
||||
$photo = Photo::factory()->for($event)->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
$style = AiStyle::query()->create([
|
||||
'key' => 'tenant-budget-cap-style',
|
||||
'name' => 'Tenant Budget Cap',
|
||||
'provider' => 'runware',
|
||||
'provider_model' => 'runware-default',
|
||||
'requires_source_image' => true,
|
||||
'is_active' => true,
|
||||
]);
|
||||
AiUsageLedger::query()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'event_id' => $event->id,
|
||||
'entry_type' => AiUsageLedger::TYPE_DEBIT,
|
||||
'quantity' => 1,
|
||||
'unit_cost_usd' => 0.02,
|
||||
'amount_usd' => 0.02,
|
||||
'currency' => 'USD',
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/ai-edits", [
|
||||
'photo_id' => $photo->id,
|
||||
'style_id' => $style->id,
|
||||
'prompt' => 'Apply AI style.',
|
||||
'idempotency_key' => 'tenant-budget-cap-1',
|
||||
]);
|
||||
|
||||
$response->assertForbidden()
|
||||
->assertJsonPath('error.code', 'budget_hard_cap_reached')
|
||||
->assertJsonPath('error.meta.budget.hard_reached', true);
|
||||
}
|
||||
|
||||
public function test_tenant_prompt_block_records_abuse_metadata_when_escalation_threshold_is_reached(): void
|
||||
{
|
||||
config([
|
||||
'ai-editing.abuse.escalation_threshold_per_hour' => 1,
|
||||
'ai-editing.abuse.escalation_cooldown_minutes' => 1,
|
||||
]);
|
||||
AiEditingSetting::flushCache();
|
||||
AiEditingSetting::query()->create(array_merge(
|
||||
AiEditingSetting::defaults(),
|
||||
['blocked_terms' => ['weapon']]
|
||||
));
|
||||
|
||||
$event = Event::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'published',
|
||||
]);
|
||||
$this->attachEntitledEventPackage($event);
|
||||
|
||||
$photo = Photo::factory()->for($event)->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
$style = AiStyle::query()->create([
|
||||
'key' => 'tenant-escalation-style',
|
||||
'name' => 'Tenant Escalation',
|
||||
'provider' => 'runware',
|
||||
'provider_model' => 'runware-default',
|
||||
'requires_source_image' => true,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/ai-edits", [
|
||||
'photo_id' => $photo->id,
|
||||
'style_id' => $style->id,
|
||||
'prompt' => 'Add weapon effects.',
|
||||
'idempotency_key' => 'tenant-escalation-1',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.status', AiEditRequest::STATUS_BLOCKED)
|
||||
->assertJsonFragment(['abuse_escalation_threshold_reached']);
|
||||
|
||||
$requestId = (int) $response->json('data.id');
|
||||
$editRequest = AiEditRequest::query()->find($requestId);
|
||||
|
||||
$this->assertNotNull($editRequest);
|
||||
$this->assertIsArray($editRequest?->metadata);
|
||||
$this->assertSame('prompt_block', $editRequest?->metadata['abuse']['type'] ?? null);
|
||||
$this->assertTrue((bool) ($editRequest?->metadata['abuse']['escalated'] ?? false));
|
||||
}
|
||||
|
||||
public function test_tenant_cannot_create_ai_edit_when_event_ai_feature_is_disabled(): void
|
||||
{
|
||||
$event = Event::factory()->create([
|
||||
@@ -685,7 +829,10 @@ class TenantAiEditControllerTest extends TenantTestCase
|
||||
->assertJsonPath('data.total', 2)
|
||||
->assertJsonPath('data.status_counts.succeeded', 1)
|
||||
->assertJsonPath('data.status_counts.failed', 1)
|
||||
->assertJsonPath('data.failed_total', 1);
|
||||
->assertJsonPath('data.failed_total', 1)
|
||||
->assertJsonPath('data.usage.debit_count', 0)
|
||||
->assertJsonPath('data.observability.provider_runs_total', 0)
|
||||
->assertJsonPath('data.budget.hard_stop_enabled', true);
|
||||
}
|
||||
|
||||
private function attachEntitledEventPackage(Event $event): EventPackage
|
||||
|
||||
Reference in New Issue
Block a user