set('scoring.content_score.provider', 'openai'); config()->set('services.openai.api_key', 'test-key'); Http::fake([ '*' => Http::response([ 'choices' => [ ['message' => ['content' => json_encode([ 'score' => $score, 'breakdown' => ['pressestil' => 15], ])]], ], ], 200), ]); } test('tier mapping follows the configured thresholds', function () { config()->set('scoring.content_score.tiers', ['gepruft' => 60, 'hochwertig' => 80]); expect(PressReleaseContentTier::fromScore(45))->toBe(PressReleaseContentTier::Standard); expect(PressReleaseContentTier::fromScore(60))->toBe(PressReleaseContentTier::Geprueft); expect(PressReleaseContentTier::fromScore(79))->toBe(PressReleaseContentTier::Geprueft); expect(PressReleaseContentTier::fromScore(80))->toBe(PressReleaseContentTier::Hochwertig); }); test('only gepruft and hochwertig are publicly badged', function () { expect(PressReleaseContentTier::Standard->isPubliclyBadged())->toBeFalse(); expect(PressReleaseContentTier::Geprueft->isPubliclyBadged())->toBeTrue(); expect(PressReleaseContentTier::Hochwertig->isPubliclyBadged())->toBeTrue(); }); test('score job stores the openai score, derives the tier and writes an audit', function () { fakeOpenAiScore(72); $pressRelease = PressRelease::factory()->create(); (new ScorePressRelease($pressRelease->id))->handle(app(ContentScoreManager::class)); $fresh = $pressRelease->fresh(); expect($fresh->content_score)->toBe(72); expect($fresh->content_tier)->toBe(PressReleaseContentTier::Geprueft); expect($fresh->scored_at)->not->toBeNull(); $audit = KiAudit::where('press_release_id', $pressRelease->id) ->where('type', KiAudit::TYPE_CONTENT_SCORE) ->firstOrFail(); expect($audit->provider)->toBe('openai'); expect($audit->result)->toBe('72'); }); test('score job falls back to the deterministic driver when openai fails', function () { config()->set('scoring.content_score.provider', 'openai'); config()->set('services.openai.api_key', 'test-key'); Http::fake(['*' => Http::response('error', 500)]); $pressRelease = PressRelease::factory()->create([ 'title' => 'Eine ausreichend lange und klare Pressemitteilungs-Headline', 'text' => str_repeat('Inhaltlicher Satz mit Substanz. ', 80), ]); (new ScorePressRelease($pressRelease->id))->handle(app(ContentScoreManager::class)); $audit = KiAudit::where('press_release_id', $pressRelease->id) ->where('type', KiAudit::TYPE_CONTENT_SCORE) ->firstOrFail(); expect($audit->provider)->toBe('deterministic'); expect($pressRelease->fresh()->content_score)->toBeGreaterThan(0); }); test('submitForReview dispatches the scoring job onto the classification queue', function () { Queue::fake(); config()->set('blacklist.words', []); $user = User::factory()->create(); $pressRelease = PressRelease::factory()->create([ 'user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value, 'title' => 'Sauber', 'text' => 'Inhalt', ]); app(PressReleaseService::class)->submitForReview($pressRelease); Queue::assertPushedOn('classification', ScorePressRelease::class, function (ScorePressRelease $job) use ($pressRelease) { return $job->pressReleaseId === $pressRelease->id; }); }); test('rescoreIfScored dispatches only for an already scored press release', function () { Queue::fake(); $scored = PressRelease::factory()->create(['content_score' => 55]); $neverScored = PressRelease::factory()->create(['content_score' => null]); app(PressReleaseService::class)->rescoreIfScored($scored); app(PressReleaseService::class)->rescoreIfScored($neverScored); Queue::assertPushed(ScorePressRelease::class, 1); });