Files
fotospiel-app/tests/Feature/Console/AiEditsRecoverStuckCommandTest.php
Codex Agent 1d2242fb4d
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
feat(ai): finalize AI magic edits epic rollout and operations
2026-02-06 22:41:51 +01:00

126 lines
4.2 KiB
PHP

<?php
namespace Tests\Feature\Console;
use App\Jobs\PollAiEditRequest;
use App\Jobs\ProcessAiEditRequest;
use App\Models\AiEditRequest;
use App\Models\AiProviderRun;
use App\Models\AiStyle;
use App\Models\Event;
use App\Models\Photo;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class AiEditsRecoverStuckCommandTest extends TestCase
{
use RefreshDatabase;
public function test_command_requeues_stuck_requests_with_scope_aware_job_selection(): void
{
[$queuedRequest, $processingRequest] = $this->createStuckRequests();
Queue::fake();
$this->artisan('ai-edits:recover-stuck', [
'--minutes' => 10,
'--requeue' => true,
])->assertExitCode(0);
Queue::assertPushed(ProcessAiEditRequest::class, 1);
Queue::assertPushed(PollAiEditRequest::class, 1);
$this->assertDatabaseHas('ai_edit_requests', [
'id' => $queuedRequest->id,
'status' => AiEditRequest::STATUS_QUEUED,
]);
$this->assertDatabaseHas('ai_edit_requests', [
'id' => $processingRequest->id,
'status' => AiEditRequest::STATUS_PROCESSING,
]);
}
public function test_command_can_mark_stuck_requests_as_failed(): void
{
[$queuedRequest, $processingRequest] = $this->createStuckRequests();
$this->artisan('ai-edits:recover-stuck', [
'--minutes' => 10,
'--fail' => true,
])->assertExitCode(0);
$this->assertDatabaseHas('ai_edit_requests', [
'id' => $queuedRequest->id,
'status' => AiEditRequest::STATUS_FAILED,
'failure_code' => 'operator_recovery_marked_failed',
]);
$this->assertDatabaseHas('ai_edit_requests', [
'id' => $processingRequest->id,
'status' => AiEditRequest::STATUS_FAILED,
'failure_code' => 'operator_recovery_marked_failed',
]);
}
/**
* @return array{0: AiEditRequest, 1: AiEditRequest}
*/
private function createStuckRequests(): 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' => 'recovery-style',
'name' => 'Recovery Style',
'provider' => 'runware',
'provider_model' => 'runware-default',
'requires_source_image' => true,
'is_active' => true,
]);
$queued = 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' => 'stuck-queued-1',
'queued_at' => now()->subMinutes(45),
'updated_at' => now()->subMinutes(45),
]);
$processing = 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' => 'stuck-processing-1',
'queued_at' => now()->subMinutes(50),
'started_at' => now()->subMinutes(40),
'updated_at' => now()->subMinutes(40),
]);
AiProviderRun::query()->create([
'request_id' => $processing->id,
'provider' => 'runware',
'attempt' => 1,
'provider_task_id' => 'runware-task-recovery-1',
'status' => AiProviderRun::STATUS_RUNNING,
'started_at' => now()->subMinutes(39),
]);
return [$queued, $processing];
}
}