feat(ai): finalize AI magic edits epic rollout and operations
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-06 22:41:51 +01:00
parent 36bed12ff9
commit 1d2242fb4d
33 changed files with 2621 additions and 18 deletions

View File

@@ -9,7 +9,10 @@ use App\Models\AiStyle;
use App\Models\AiUsageLedger;
use App\Models\Event;
use App\Models\Photo;
use App\Services\AiEditing\AiProviderResult;
use App\Services\AiEditing\Providers\RunwareAiImageProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;
class ProcessAiEditRequestTest extends TestCase
@@ -187,5 +190,106 @@ class ProcessAiEditRequestTest extends TestCase
$this->assertNotNull($request->completed_at);
$this->assertSame(0, $request->outputs()->count());
$this->assertSame(1, $request->providerRuns()->count());
$this->assertIsArray($request->metadata);
$this->assertSame('output_block', $request->metadata['abuse']['type'] ?? null);
}
public function test_it_marks_request_failed_when_provider_returns_processing_without_task_id(): void
{
AiEditingSetting::query()->create(array_merge(
AiEditingSetting::defaults(),
[
'runware_mode' => 'live',
'queue_auto_dispatch' => false,
]
));
$event = Event::factory()->create(['status' => 'published']);
$photo = Photo::factory()->for($event)->create([
'tenant_id' => $event->tenant_id,
'status' => 'approved',
]);
$style = AiStyle::query()->create([
'key' => 'processing-no-task',
'name' => 'Processing Without Task ID',
'provider' => 'runware',
'provider_model' => 'runware-default',
'requires_source_image' => true,
'is_active' => true,
]);
$request = 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_QUEUED,
'safety_state' => 'pending',
'prompt' => 'Transform image style.',
'idempotency_key' => 'job-processing-no-task',
'queued_at' => now(),
]);
$provider = \Mockery::mock(RunwareAiImageProvider::class);
$provider->shouldReceive('submit')
->once()
->andReturn(new AiProviderResult(status: 'processing', providerTaskId: ''));
$this->app->instance(RunwareAiImageProvider::class, $provider);
ProcessAiEditRequest::dispatchSync($request->id);
$request->refresh();
$run = $request->providerRuns()->first();
$this->assertSame(AiEditRequest::STATUS_FAILED, $request->status);
$this->assertSame('provider_task_id_missing', $request->failure_code);
$this->assertNotNull($request->completed_at);
$this->assertNotNull($run);
$this->assertSame('failed', $run?->status);
$this->assertSame('Provider returned processing state without task identifier.', $run?->error_message);
}
public function test_process_job_failed_hook_marks_request_as_failed(): void
{
$event = Event::factory()->create(['status' => 'published']);
$photo = Photo::factory()->for($event)->create([
'tenant_id' => $event->tenant_id,
'status' => 'approved',
]);
$style = AiStyle::query()->create([
'key' => 'failed-hook-style',
'name' => 'Failed Hook Style',
'provider' => 'runware',
'provider_model' => 'runware-default',
'requires_source_image' => true,
'is_active' => true,
]);
$request = 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_PROCESSING,
'safety_state' => 'pending',
'prompt' => 'Transform image style.',
'idempotency_key' => 'job-failed-hook-1',
'queued_at' => now()->subMinute(),
'started_at' => now()->subSeconds(30),
]);
$job = new ProcessAiEditRequest($request->id);
$job->failed(new RuntimeException('Queue worker timeout'));
$request->refresh();
$this->assertSame(AiEditRequest::STATUS_FAILED, $request->status);
$this->assertSame('queue_job_failed', $request->failure_code);
$this->assertSame('Queue worker timeout', $request->failure_message);
$this->assertNotNull($request->completed_at);
}
}