0.6, 'nature' => 0.4], categoryWeights: ['wallpapers' => 1.0], ); $arr = $dto->toArray(); $restored = UserRecoProfileDTO::fromArray($arr); expect($restored->topTagSlugs)->toBe(['space', 'nature']) ->and($restored->topCategorySlugs)->toBe(['wallpapers']) ->and($restored->strongCreatorIds)->toBe([1, 2, 3]) ->and($restored->tagWeight('space'))->toBe(0.6) ->and($restored->followsCreator(2))->toBeTrue() ->and($restored->followsCreator(99))->toBeFalse(); }); it('DTO hasSignals returns false for empty profile', function () { $empty = new UserRecoProfileDTO(); expect($empty->hasSignals())->toBeFalse(); }); it('DTO hasSignals returns true when tags are present', function () { $dto = new UserRecoProfileDTO(topTagSlugs: ['space']); expect($dto->hasSignals())->toBeTrue(); }); // ───────────────────────────────────────────────────────────────────────────── // UserPreferenceBuilder // ───────────────────────────────────────────────────────────────────────────── it('UserPreferenceBuilder returns empty DTO for user with no activity', function () { $user = User::factory()->create(); $builder = app(UserPreferenceBuilder::class); $dto = $builder->build($user); expect($dto)->toBeInstanceOf(UserRecoProfileDTO::class) ->and($dto->topTagSlugs)->toBe([]) ->and($dto->strongCreatorIds)->toBe([]); }); it('UserPreferenceBuilder persists profile row on first build', function () { $user = User::factory()->create(); $builder = app(UserPreferenceBuilder::class); $builder->buildFresh($user); expect(UserRecoProfile::find($user->id))->not->toBeNull(); }); it('UserPreferenceBuilder produces stable output on repeated calls', function () { $user = User::factory()->create(); $builder = app(UserPreferenceBuilder::class); $first = $builder->buildFresh($user)->toArray(); $second = $builder->buildFresh($user)->toArray(); expect($first)->toBe($second); });