657 lines
22 KiB
PHP
657 lines
22 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\Category;
|
|
use App\Models\ContentType;
|
|
use App\Models\UploadBatch;
|
|
use App\Models\UploadBatchItem;
|
|
use App\Models\User;
|
|
use App\Jobs\AnalyzeArtworkAiAssistJob;
|
|
use App\Jobs\AutoTagArtworkJob;
|
|
use App\Jobs\DetectArtworkMaturityJob;
|
|
use App\Jobs\GenerateArtworkEmbeddingJob;
|
|
use App\Repositories\Uploads\UploadSessionRepository;
|
|
use App\Services\Maturity\ArtworkMaturityService;
|
|
use App\Services\TagService;
|
|
use App\Services\Uploads\UploadQueueService;
|
|
use App\Services\Uploads\UploadSessionStatus;
|
|
use App\Services\Uploads\UploadTokenService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function uploadQueueArtwork(array $attributes = []): Artwork
|
|
{
|
|
return Artwork::withoutEvents(fn () => Artwork::factory()->create($attributes));
|
|
}
|
|
|
|
function uploadQueueCategory(string $typeName = 'Photography', string $categoryName = 'Portraits'): Category
|
|
{
|
|
$suffix = Str::lower(Str::random(6));
|
|
|
|
$contentType = ContentType::query()->create([
|
|
'name' => $typeName,
|
|
'slug' => Str::slug($typeName) . '-' . $suffix,
|
|
'order' => 1,
|
|
'hide_from_menu' => false,
|
|
]);
|
|
|
|
return Category::query()->create([
|
|
'content_type_id' => $contentType->id,
|
|
'name' => $categoryName,
|
|
'slug' => Str::slug($categoryName) . '-' . $suffix,
|
|
'is_active' => true,
|
|
'sort_order' => 1,
|
|
]);
|
|
}
|
|
|
|
beforeEach(function () {
|
|
if (DB::connection()->getDriverName() === 'sqlite') {
|
|
DB::connection()->getPdo()->sqliteCreateFunction('GREATEST', function (...$args) {
|
|
return max($args);
|
|
}, -1);
|
|
}
|
|
|
|
$this->user = User::factory()->create();
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
test('studio upload queue page loads', function () {
|
|
$this->get('/studio/upload-queue')
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Studio/StudioUploadQueue')
|
|
->where('title', 'Upload Queue')
|
|
->has('queue.status_options')
|
|
->has('queue.sort_options'));
|
|
});
|
|
|
|
test('upload queue batch creation creates draft artworks and queue items with defaults', function () {
|
|
$contentType = ContentType::query()->create([
|
|
'name' => 'Photography',
|
|
'slug' => 'photography',
|
|
'order' => 1,
|
|
'hide_from_menu' => false,
|
|
]);
|
|
|
|
$category = Category::query()->create([
|
|
'content_type_id' => $contentType->id,
|
|
'name' => 'Landscapes',
|
|
'slug' => 'landscapes',
|
|
'is_active' => true,
|
|
'sort_order' => 1,
|
|
]);
|
|
|
|
$response = $this->postJson('/api/studio/upload-queue/batches', [
|
|
'name' => 'Spring Set',
|
|
'files' => [
|
|
['name' => 'forest-light.png'],
|
|
['name' => 'city-night.webp'],
|
|
],
|
|
'defaults' => [
|
|
'category_id' => $category->id,
|
|
'tags' => ['forest', 'set'],
|
|
'visibility' => 'unlisted',
|
|
],
|
|
]);
|
|
|
|
$response->assertCreated()
|
|
->assertJsonPath('batch.name', 'Spring Set')
|
|
->assertJsonCount(2, 'items');
|
|
|
|
$batch = UploadBatch::query()->firstOrFail();
|
|
expect($batch->total_items)->toBe(2);
|
|
|
|
$items = UploadBatchItem::query()->with(['artwork.categories', 'artwork.tags'])->get();
|
|
expect($items)->toHaveCount(2);
|
|
|
|
foreach ($items as $item) {
|
|
expect($item->artwork)->not->toBeNull()
|
|
->and($item->artwork->visibility)->toBe('unlisted')
|
|
->and($item->artwork->categories->pluck('id')->all())->toBe([$category->id])
|
|
->and($item->artwork->tags->pluck('slug')->sort()->values()->all())->toBe(['forest', 'set']);
|
|
}
|
|
});
|
|
|
|
test('upload finish updates queue item when batch item id is supplied', function () {
|
|
config()->set('forum_bot_protection.enabled', false);
|
|
config()->set('uploads.queue_derivatives', false);
|
|
config()->set('uploads.storage_root', storage_path('framework/testing/uploads'));
|
|
|
|
Queue::fake();
|
|
File::deleteDirectory((string) config('uploads.storage_root'));
|
|
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Queue batch',
|
|
'status' => 'uploading',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$artwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'is_public' => false,
|
|
'is_approved' => false,
|
|
'published_at' => null,
|
|
'artwork_status' => 'draft',
|
|
]);
|
|
|
|
$item = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => 'queue-test.png',
|
|
]);
|
|
|
|
$sessionId = (string) Str::uuid();
|
|
$tmpPath = storage_path('framework/testing/uploads/tmp/' . $sessionId . '.png');
|
|
$sourceImage = base_path('public/favicon/favicon-96x96.png');
|
|
File::ensureDirectoryExists(dirname($tmpPath));
|
|
File::copy($sourceImage, $tmpPath);
|
|
|
|
app(UploadSessionRepository::class)->create(
|
|
$sessionId,
|
|
$this->user->id,
|
|
$tmpPath,
|
|
UploadSessionStatus::TMP,
|
|
'127.0.0.1'
|
|
);
|
|
|
|
$token = app(UploadTokenService::class)->generate($sessionId, $this->user->id);
|
|
|
|
$this->withHeader('X-Upload-Token', $token)
|
|
->postJson('/api/uploads/finish', [
|
|
'session_id' => $sessionId,
|
|
'artwork_id' => $artwork->id,
|
|
'batch_item_id' => $item->id,
|
|
'file_name' => 'queue-test.png',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('artwork_id', $artwork->id)
|
|
->assertJsonPath('status', UploadSessionStatus::PROCESSED);
|
|
|
|
$item->refresh();
|
|
|
|
expect($item->status)->toBe('processing')
|
|
->and($item->processing_stage)->toBe('maturity_check');
|
|
});
|
|
|
|
test('upload queue bulk publish only publishes ready items', function () {
|
|
$contentType = ContentType::query()->create([
|
|
'name' => 'Photography',
|
|
'slug' => 'photography',
|
|
'order' => 1,
|
|
'hide_from_menu' => false,
|
|
]);
|
|
|
|
$category = Category::query()->create([
|
|
'content_type_id' => $contentType->id,
|
|
'name' => 'Portraits',
|
|
'slug' => 'portraits',
|
|
'is_active' => true,
|
|
'sort_order' => 1,
|
|
]);
|
|
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Publish batch',
|
|
'status' => 'processing',
|
|
'total_items' => 2,
|
|
]);
|
|
|
|
$readyArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Ready artwork',
|
|
'file_name' => 'ready.webp',
|
|
'file_path' => 'artworks/test/ready.webp',
|
|
'hash' => str_repeat('a', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'visibility' => 'public',
|
|
'is_public' => false,
|
|
'is_approved' => false,
|
|
'artwork_status' => 'draft',
|
|
'published_at' => null,
|
|
'maturity_status' => 'clear',
|
|
'maturity_ai_status' => 'succeeded',
|
|
]);
|
|
$readyArtwork->categories()->sync([$category->id]);
|
|
|
|
$blockedArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Blocked artwork',
|
|
'file_name' => 'blocked.webp',
|
|
'file_path' => 'artworks/test/blocked.webp',
|
|
'hash' => str_repeat('b', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'visibility' => 'public',
|
|
'is_public' => false,
|
|
'is_approved' => false,
|
|
'artwork_status' => 'draft',
|
|
'published_at' => null,
|
|
'maturity_status' => 'suspected',
|
|
'maturity_ai_status' => 'succeeded',
|
|
]);
|
|
$blockedArtwork->categories()->sync([$category->id]);
|
|
|
|
$readyItem = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $readyArtwork->id,
|
|
'original_filename' => 'ready.webp',
|
|
]);
|
|
|
|
$blockedItem = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $blockedArtwork->id,
|
|
'original_filename' => 'blocked.webp',
|
|
]);
|
|
|
|
$this->postJson('/api/studio/upload-queue/bulk', [
|
|
'action' => 'publish',
|
|
'item_ids' => [$readyItem->id, $blockedItem->id],
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('success', 1)
|
|
->assertJsonPath('failed', 1);
|
|
|
|
$readyArtwork->refresh();
|
|
$blockedArtwork->refresh();
|
|
|
|
expect($readyArtwork->artwork_status)->toBe('published')
|
|
->and($readyArtwork->published_at)->not->toBeNull()
|
|
->and($blockedArtwork->artwork_status)->toBe('draft')
|
|
->and($blockedArtwork->published_at)->toBeNull();
|
|
});
|
|
|
|
test('upload queue bulk delete only affects owned drafts', function () {
|
|
$otherUser = User::factory()->create();
|
|
|
|
$ownedBatch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Delete batch',
|
|
'status' => 'processing',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$ownedArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
]);
|
|
|
|
$foreignBatch = UploadBatch::query()->create([
|
|
'user_id' => $otherUser->id,
|
|
'name' => 'Foreign batch',
|
|
'status' => 'processing',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$foreignArtwork = uploadQueueArtwork([
|
|
'user_id' => $otherUser->id,
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
]);
|
|
|
|
$ownedItem = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $ownedBatch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $ownedArtwork->id,
|
|
'original_filename' => 'owned.webp',
|
|
]);
|
|
|
|
$foreignItem = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $foreignBatch->id,
|
|
'user_id' => $otherUser->id,
|
|
'artwork_id' => $foreignArtwork->id,
|
|
'original_filename' => 'foreign.webp',
|
|
]);
|
|
|
|
$this->postJson('/api/studio/upload-queue/bulk', [
|
|
'action' => 'delete',
|
|
'item_ids' => [$ownedItem->id, $foreignItem->id],
|
|
'confirm' => 'DELETE',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('success', 1);
|
|
|
|
$ownedItem->refresh();
|
|
$foreignItem->refresh();
|
|
|
|
expect($ownedItem->status)->toBe('deleted')
|
|
->and(Artwork::withTrashed()->find($ownedArtwork->id)?->trashed())->toBeTrue()
|
|
->and($foreignItem->status)->not->toBe('deleted')
|
|
->and(Artwork::find($foreignArtwork->id))->not->toBeNull();
|
|
});
|
|
|
|
test('upload queue retry rejects drafts without processed media', function () {
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Retry batch',
|
|
'status' => 'completed_with_errors',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$artwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'file_path' => '',
|
|
'hash' => '',
|
|
]);
|
|
|
|
$item = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => 'retry.webp',
|
|
'status' => 'failed',
|
|
]);
|
|
|
|
$this->postJson('/api/studio/upload-queue/items/' . $item->id . '/retry')
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['item']);
|
|
});
|
|
|
|
test('upload queue item failure does not break the rest of the batch', function () {
|
|
$response = $this->postJson('/api/studio/upload-queue/batches', [
|
|
'name' => 'Mixed batch',
|
|
'files' => [
|
|
['name' => 'good.webp'],
|
|
['name' => 'bad.webp'],
|
|
],
|
|
]);
|
|
|
|
$batchId = (int) $response->json('batch.id');
|
|
$items = UploadBatchItem::query()->where('upload_batch_id', $batchId)->orderBy('id')->get();
|
|
|
|
expect($items)->toHaveCount(2);
|
|
|
|
$this->postJson('/api/studio/upload-queue/items/' . $items[1]->id . '/fail', [
|
|
'error_code' => 'invalid_file',
|
|
'error_message' => 'Invalid image payload.',
|
|
])->assertOk();
|
|
|
|
$payload = app(UploadQueueService::class)->listPayload($this->user, ['batch_id' => $batchId]);
|
|
$queueItems = collect($payload['items'])->keyBy('id');
|
|
|
|
expect($queueItems)->toHaveCount(2)
|
|
->and($queueItems[$items[0]->id]['status'])->not->toBe('failed')
|
|
->and($queueItems[$items[1]->id]['status'])->toBe('failed')
|
|
->and($queueItems[$items[1]->id]['error_message'])->toBe('Invalid image payload.');
|
|
});
|
|
|
|
test('upload queue processing states update correctly per item', function () {
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Processing batch',
|
|
'status' => 'uploading',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$artwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Processing artwork',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_PENDING,
|
|
]);
|
|
|
|
$item = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => 'processing.webp',
|
|
'status' => 'uploaded',
|
|
'processing_stage' => 'queued',
|
|
]);
|
|
|
|
$queue = app(UploadQueueService::class);
|
|
|
|
$queued = $queue->markItemProcessingQueued($item->id);
|
|
expect($queued->status)->toBe('processing')
|
|
->and($queued->processing_stage)->toBe('thumbnails');
|
|
|
|
$artwork->forceFill([
|
|
'file_name' => 'processing.webp',
|
|
'file_path' => 'artworks/test/processing.webp',
|
|
'hash' => str_repeat('c', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
])->saveQuietly();
|
|
|
|
$processed = $queue->markItemMediaProcessed($item->id);
|
|
expect($processed->status)->toBe('processing')
|
|
->and($processed->processing_stage)->toBe('maturity_check');
|
|
});
|
|
|
|
test('upload queue publish readiness respects metadata and maturity review rules', function () {
|
|
$category = uploadQueueCategory();
|
|
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Readiness batch',
|
|
'status' => 'processing',
|
|
'total_items' => 4,
|
|
]);
|
|
|
|
$readyArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Ready artwork',
|
|
'file_name' => 'ready.webp',
|
|
'file_path' => 'artworks/test/ready.webp',
|
|
'hash' => str_repeat('d', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_CLEAR,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED,
|
|
]);
|
|
$readyArtwork->categories()->sync([$category->id]);
|
|
|
|
$missingMetadataArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => '',
|
|
'file_name' => 'metadata.webp',
|
|
'file_path' => 'artworks/test/metadata.webp',
|
|
'hash' => str_repeat('e', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_CLEAR,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED,
|
|
]);
|
|
$missingMetadataArtwork->categories()->sync([$category->id]);
|
|
|
|
$reviewArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Review artwork',
|
|
'file_name' => 'review.webp',
|
|
'file_path' => 'artworks/test/review.webp',
|
|
'hash' => str_repeat('f', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED,
|
|
]);
|
|
$reviewArtwork->categories()->sync([$category->id]);
|
|
|
|
$processingArtwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Processing artwork',
|
|
'file_name' => 'pending',
|
|
'file_path' => '',
|
|
'hash' => '',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_CLEAR,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_PENDING,
|
|
]);
|
|
$processingArtwork->categories()->sync([$category->id]);
|
|
|
|
$items = collect([
|
|
[$readyArtwork, 'ready.webp'],
|
|
[$missingMetadataArtwork, 'metadata.webp'],
|
|
[$reviewArtwork, 'review.webp'],
|
|
[$processingArtwork, 'processing.webp'],
|
|
])->map(function (array $entry) use ($batch) {
|
|
[$artwork, $filename] = $entry;
|
|
|
|
return UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => $filename,
|
|
'status' => 'processing',
|
|
'processing_stage' => 'maturity_check',
|
|
]);
|
|
});
|
|
|
|
$payload = app(UploadQueueService::class)->listPayload($this->user, ['batch_id' => $batch->id]);
|
|
$byFilename = collect($payload['items'])->keyBy('original_filename');
|
|
|
|
expect($byFilename['ready.webp']['status'])->toBe('ready')
|
|
->and($byFilename['ready.webp']['is_ready_to_publish'])->toBeTrue()
|
|
->and($byFilename['metadata.webp']['status'])->toBe('needs_metadata')
|
|
->and($byFilename['metadata.webp']['is_ready_to_publish'])->toBeFalse()
|
|
->and($byFilename['review.webp']['status'])->toBe('needs_review')
|
|
->and($byFilename['review.webp']['is_ready_to_publish'])->toBeFalse()
|
|
->and($byFilename['processing.webp']['status'])->toBe('processing')
|
|
->and($byFilename['processing.webp']['is_ready_to_publish'])->toBeFalse();
|
|
});
|
|
|
|
test('upload queue retry works for safe failure cases', function () {
|
|
Queue::fake();
|
|
|
|
$category = uploadQueueCategory();
|
|
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'Retry safe batch',
|
|
'status' => 'completed_with_errors',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$artwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Retry safe artwork',
|
|
'file_name' => 'retry-safe.webp',
|
|
'file_path' => 'artworks/test/retry-safe.webp',
|
|
'hash' => str_repeat('g', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_CLEAR,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_PENDING,
|
|
]);
|
|
$artwork->categories()->sync([$category->id]);
|
|
|
|
$item = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => 'retry-safe.webp',
|
|
'status' => 'failed',
|
|
'processing_stage' => 'finalized',
|
|
'error_code' => 'vision_timeout',
|
|
'error_message' => 'Vision analysis timed out.',
|
|
]);
|
|
|
|
$this->postJson('/api/studio/upload-queue/items/' . $item->id . '/retry')
|
|
->assertOk()
|
|
->assertJsonPath('ok', true);
|
|
|
|
$item->refresh();
|
|
|
|
expect($item->status)->toBe('processing')
|
|
->and($item->processing_stage)->toBe('maturity_check')
|
|
->and($item->error_code)->toBeNull()
|
|
->and($item->error_message)->toBeNull();
|
|
|
|
Queue::assertPushed(AutoTagArtworkJob::class);
|
|
Queue::assertPushed(DetectArtworkMaturityJob::class);
|
|
Queue::assertPushed(GenerateArtworkEmbeddingJob::class);
|
|
Queue::assertPushed(AnalyzeArtworkAiAssistJob::class);
|
|
});
|
|
|
|
test('upload queue AI generation does not overwrite manual metadata silently', function () {
|
|
Queue::fake();
|
|
|
|
$category = uploadQueueCategory();
|
|
|
|
$batch = UploadBatch::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'name' => 'AI batch',
|
|
'status' => 'completed_with_errors',
|
|
'total_items' => 1,
|
|
]);
|
|
|
|
$artwork = uploadQueueArtwork([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Manual title',
|
|
'description' => 'Manual description',
|
|
'file_name' => 'manual.webp',
|
|
'file_path' => 'artworks/test/manual.webp',
|
|
'hash' => str_repeat('h', 64),
|
|
'thumb_ext' => 'webp',
|
|
'file_ext' => 'webp',
|
|
'artwork_status' => 'draft',
|
|
'is_public' => false,
|
|
'published_at' => null,
|
|
'maturity_status' => ArtworkMaturityService::STATUS_CLEAR,
|
|
'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED,
|
|
]);
|
|
$artwork->categories()->sync([$category->id]);
|
|
app(TagService::class)->syncStudioTags($artwork, ['manual-tag']);
|
|
|
|
$item = UploadBatchItem::query()->create([
|
|
'upload_batch_id' => $batch->id,
|
|
'user_id' => $this->user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'original_filename' => 'manual.webp',
|
|
'status' => 'failed',
|
|
'processing_stage' => 'finalized',
|
|
'error_code' => 'metadata_failed',
|
|
'error_message' => 'AI metadata generation failed.',
|
|
]);
|
|
|
|
$this->postJson('/api/studio/upload-queue/bulk', [
|
|
'action' => 'generate_ai',
|
|
'item_ids' => [$item->id],
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('success', 1)
|
|
->assertJsonPath('failed', 0);
|
|
|
|
$artwork->refresh();
|
|
|
|
expect($artwork->title)->toBe('Manual title')
|
|
->and($artwork->description)->toBe('Manual description')
|
|
->and($artwork->categories()->pluck('categories.id')->all())->toBe([$category->id])
|
|
->and($artwork->tags()->pluck('tags.slug')->all())->toBe(['manual-tag']);
|
|
|
|
Queue::assertPushed(AnalyzeArtworkAiAssistJob::class);
|
|
}); |