seed(RolesAndPermissionsSeeder::class); config()->set('scoring.classification.max_automatic_attempts', 2); config()->set('billing.enforce_booking', false); Queue::fake(); Mail::fake(); }); function gateDraft(): array { $customer = User::factory()->create(['is_active' => true]); $customer->assignRole('customer'); $pr = PressRelease::factory()->create(['user_id' => $customer->id, 'status' => PressReleaseStatus::Draft->value]); return [$customer, $pr]; } function redRun(PressRelease $pr, string $reason): void { app(PressReleaseService::class)->routeByClassification($pr->fresh(), PressReleaseClassification::Red, $reason); } test('each release counts its automatic checks and the last red blocks instead of rejecting', function () { /** @var TestCase $this */ [$customer, $pr] = gateDraft(); $service = app(PressReleaseService::class); $this->actingAs($customer); // Erster Durchlauf: Rot → abgelehnt, Zähler 1. $service->submitForReview($pr); Queue::assertPushed(ClassifyPressRelease::class, 1); expect($pr->fresh()->auto_check_attempts)->toBe(1); redRun($pr, 'K1: Werbliche Sprache'); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Rejected) ->and($pr->fresh()->requiresManualReview())->toBeFalse(); // Zweiter (= letzter erlaubter) Durchlauf: Rot → nicht ablehnen, blocken. $service->submitForReview($pr->fresh()); Queue::assertPushed(ClassifyPressRelease::class, 2); expect($pr->fresh()->auto_check_attempts)->toBe(2); redRun($pr, 'K1: Immer noch werblich'); $fresh = $pr->fresh(); expect($fresh->status)->toBe(PressReleaseStatus::Review) ->and($fresh->isAwaitingManualReview())->toBeTrue() ->and(PressReleaseStatusLog::query()->where('press_release_id', $pr->id)->latest('id')->first()->reason) ->toContain('Automatische Prüfung erschöpft (2 von 2)'); // Keine Ablehnungsmail für den Block — der Autor sieht den Stand im Konto. Mail::assertNotQueued(PressReleaseRejected::class, fn ($mail) => str_contains((string) $mail->reason, 'erschöpft')); }); test('after the human decision the automation stays off for this release', function () { /** @var TestCase $this */ [$customer, $pr] = gateDraft(); $service = app(PressReleaseService::class); $this->actingAs($customer); $pr->update(['auto_check_attempts' => 2, 'status' => PressReleaseStatus::Review->value]); redRun($pr, 'K2: Rot'); expect($pr->fresh()->isAwaitingManualReview())->toBeTrue(); // Admin: mit Kommentar zurück. $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $service->reject($pr->fresh(), 'Bitte die Behauptung im zweiten Absatz belegen.'); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Rejected) ->and($pr->fresh()->requiresManualReview())->toBeTrue(); // Autor gibt erneut frei: kein KI-Job, direkt wieder in der Queue. $this->actingAs($customer); Queue::fake(); $service->submitForReview($pr->fresh()); Queue::assertNotPushed(ClassifyPressRelease::class); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Review) ->and($pr->fresh()->isAwaitingManualReview())->toBeTrue() ->and($pr->fresh()->auto_check_attempts)->toBe(2) ->and(PressReleaseStatusLog::query()->where('press_release_id', $pr->id)->latest('id')->first()->reason)->toContain('wartet auf manuelle Entscheidung'); }); test('yellow publishes by default and is held only when the switch is on', function () { /** @var TestCase $this */ [, $pr] = gateDraft(); $pr->update(['status' => PressReleaseStatus::Review->value]); config()->set('scoring.classification.yellow_to_manual_queue', false); app(PressReleaseService::class)->routeByClassification($pr->fresh(), PressReleaseClassification::Yellow, 'Gelb'); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Published); [, $other] = gateDraft(); $other->update(['status' => PressReleaseStatus::Review->value]); config()->set('scoring.classification.yellow_to_manual_queue', true); app(PressReleaseService::class)->routeByClassification($other->fresh(), PressReleaseClassification::Yellow, 'Gelb'); expect($other->fresh()->status)->toBe(PressReleaseStatus::Review) ->and($other->fresh()->isAwaitingManualReview())->toBeTrue(); }); test('the admin sees blocked releases in their own view and the detail page explains the decision', function () { /** @var TestCase $this */ [, $pr] = gateDraft(); $pr->update(['auto_check_attempts' => 2, 'status' => PressReleaseStatus::Review->value, 'title' => 'Geblockte Meldung']); redRun($pr, 'K4: Falsche Tatsachenbehauptung'); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); LivewireVolt::test('admin.press-releases.index') ->call('setView', 'manual') ->assertSeeText('Geblockte Meldung') ->assertSeeText('Prüfung nötig'); LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id]) ->assertSeeText('Automatik erschöpft') ->assertSeeText('K4: Falsche Tatsachenbehauptung') ->assertSeeText('Freischalten') ->assertSeeText('Mit Kommentar zurück'); }); test('the customer page explains the release button, counts attempts and shows the human review', function () { /** @var TestCase $this */ [$customer, $pr] = gateDraft(); $this->actingAs($customer); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->assertSeeText('Zur Prüfung freigeben') ->assertSeeText('Was nach der Freigabe passiert') ->assertSeeText('Nach 2 Prüfungen mit Rot'); $pr->update(['status' => PressReleaseStatus::Rejected->value, 'auto_check_attempts' => 1]); PressReleaseStatusLog::query()->create(['press_release_id' => $pr->id, 'from_status' => 'review', 'to_status' => 'rejected', 'reason' => 'K1: Werblich', 'source' => 'ki']); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->assertSeeText('Automatische Prüfung 1 von 2 genutzt'); $pr->update(['status' => PressReleaseStatus::Review->value, 'auto_check_attempts' => 2, 'manual_review_required_at' => now()]); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->assertSeeText('von einem Menschen geprüft'); });