create(); $service = app(CollectionHealthService::class); $collection = Collection::factory()->make([ 'user_id' => $user->id, 'title' => 'Stale Health Candidate', 'slug' => 'stale-health-candidate', 'mode' => Collection::MODE_SMART, 'artworks_count' => 6, 'visibility' => Collection::VISIBILITY_PUBLIC, 'moderation_status' => Collection::MODERATION_ACTIVE, 'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED, 'summary' => 'A focused summary for stale health coverage.', 'description' => 'A longer description that gives the stale collection enough metadata depth for the health service.', 'likes_count' => 45, 'followers_count' => 20, 'saves_count' => 24, 'comments_count' => 6, 'shares_count' => 4, 'views_count' => 1000, 'last_activity_at' => now()->subDays(60), 'updated_at' => now()->subDays(60), 'published_at' => now()->subDays(60), 'workflow_state' => Collection::WORKFLOW_APPROVED, ]); $collection->cover_artwork_id = 999; $collection->setRelation('coverArtwork', Artwork::factory()->for($user)->make([ 'width' => 1400, 'height' => 900, 'published_at' => now()->subDays(61), ])); $flagsMethod = new ReflectionMethod(CollectionHealthService::class, 'flags'); $flagsMethod->setAccessible(true); $flags = $flagsMethod->invoke($service, $collection, 80.0, 0.0, 80.0, 80.0); expect($flags)->toContain(Collection::HEALTH_STALE); }); it('flags collections with fewer than six artworks as low content', function (): void { $user = User::factory()->create(); $service = app(CollectionHealthService::class); $collection = seededHealthyCollection($user, 'Thin Collection Candidate', 5); $payload = $service->evaluate($collection->fresh(['coverArtwork'])); expect($payload['health_flags_json'])->toContain(Collection::HEALTH_LOW_CONTENT); }); it('flags broken items when too many attached artworks are not publicly visible', function (): void { $user = User::factory()->create(); $service = app(CollectionHealthService::class); $collection = Collection::factory()->for($user)->create([ 'title' => 'Broken Items Candidate', 'slug' => 'broken-items-candidate', 'visibility' => Collection::VISIBILITY_PUBLIC, 'moderation_status' => Collection::MODERATION_ACTIVE, 'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED, 'summary' => 'A collection used to validate broken item detection.', 'description' => 'A detailed curatorial description for broken item health coverage.', 'likes_count' => 40, 'followers_count' => 20, 'saves_count' => 15, 'views_count' => 900, 'published_at' => now()->subDays(10), 'updated_at' => now()->subDay(), 'last_activity_at' => now()->subDay(), ]); $visible = Artwork::factory()->for($user)->create(['width' => 1280, 'height' => 720]); $private = Artwork::factory()->for($user)->private()->create(); $unapproved = Artwork::factory()->for($user)->unapproved()->create(); $unpublished = Artwork::factory()->for($user)->unpublished()->create(); app(CollectionService::class)->attachArtworks($collection, $user, [$visible->id, $private->id, $unapproved->id, $unpublished->id]); $collection->forceFill([ 'cover_artwork_id' => $visible->id, 'updated_at' => now()->subDay(), 'last_activity_at' => now()->subDay(), ])->save(); $payload = $service->evaluate($collection->fresh(['coverArtwork'])); expect($payload['health_flags_json'])->toContain(Collection::HEALTH_BROKEN_ITEMS) ->and($payload['placement_eligibility'])->toBeFalse(); }); it('flags collections without an explicit cover as weak cover', function (): void { $user = User::factory()->create(); $service = app(CollectionHealthService::class); $collection = seededHealthyCollection($user, 'Weak Cover Candidate'); $collection->forceFill(['cover_artwork_id' => null])->save(); $payload = $service->evaluate($collection->fresh(['coverArtwork'])); expect($payload['health_flags_json'])->toContain(Collection::HEALTH_WEAK_COVER); }); it('keeps strong active collections healthy and placement eligible', function (): void { $user = User::factory()->create(); $service = app(CollectionHealthService::class); $collection = seededHealthyCollection($user, 'Healthy Cover Candidate'); $payload = $service->evaluate($collection->fresh(['coverArtwork'])); expect($payload['health_state'])->toBe(Collection::HEALTH_HEALTHY) ->and($payload['health_flags_json'])->toBe([]) ->and($payload['placement_eligibility'])->toBeTrue(); }); function seededHealthyCollection(User $user, string $title, int $artworksCount = 6): Collection { $collection = Collection::factory()->for($user)->create([ 'title' => $title, 'slug' => str($title)->slug()->value(), 'visibility' => Collection::VISIBILITY_PUBLIC, 'moderation_status' => Collection::MODERATION_ACTIVE, 'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED, 'summary' => 'A focused summary for health scoring coverage.', 'description' => 'A longer description that provides enough metadata depth for health scoring and editorial readiness calculations.', 'likes_count' => 45, 'followers_count' => 18, 'saves_count' => 24, 'comments_count' => 8, 'shares_count' => 5, 'views_count' => 1400, 'published_at' => now()->subDays(5), 'updated_at' => now()->subDay(), 'last_activity_at' => now()->subDay(), 'workflow_state' => Collection::WORKFLOW_APPROVED, ]); $artworkIds = Artwork::factory()->count($artworksCount)->for($user)->create([ 'width' => 1400, 'height' => 900, ])->pluck('id')->all(); app(CollectionService::class)->attachArtworks($collection, $user, $artworkIds); $collection->forceFill([ 'cover_artwork_id' => $artworkIds[0] ?? null, 'updated_at' => now()->subDay(), 'last_activity_at' => now()->subDay(), ])->save(); return $collection->fresh(['coverArtwork']); }