create(array_merge(['role' => 'admin'], $attributes)); $admin->forceFill([ 'isAdmin' => true, 'activated' => true, ])->save(); AdminVerification::createForUser($admin->fresh()); return $admin->fresh(); } function biographyRecord(User $user, array $attributes = []): CreatorAiBiography { return CreatorAiBiography::query()->create(array_merge([ 'user_id' => $user->id, 'text' => 'This creator has built a consistent public body of work on Skinbase, with a long-running profile, visible uploads, and a biography long enough to support admin review without tripping validation rules.', 'source_hash' => 'hash-' . fake()->unique()->numerify('####'), 'model' => 'vision-gateway', 'prompt_version' => 'v1.1', 'input_quality_tier' => CreatorAiBiography::TIER_MEDIUM, 'generation_reason' => CreatorAiBiography::REASON_ADMIN_BATCH, 'status' => CreatorAiBiography::STATUS_GENERATED, 'is_active' => true, 'is_hidden' => false, 'is_user_edited' => false, 'needs_review' => false, 'generated_at' => now()->subHour(), 'approved_at' => now()->subHour(), 'last_attempted_at' => now()->subHour(), 'last_error_code' => null, 'last_error_reason' => null, ], $attributes)); } it('blocks non staff users from the ai biography admin area', function (): void { $user = User::factory()->create(['role' => 'user']); $this->actingAs($user) ->get(route('cp.ai-biography.index')) ->assertRedirect(route('cp.login')); }); it('renders the ai biography admin index with records and stats', function (): void { $admin = aiBiographyAdminUser(); $creator = User::factory()->create(['username' => 'bioadmin']); biographyRecord($creator, [ 'needs_review' => true, 'status' => CreatorAiBiography::STATUS_NEEDS_REVIEW, ]); biographyRecord($creator, [ 'is_active' => false, 'status' => CreatorAiBiography::STATUS_FAILED, 'last_error_code' => 'generation_failed', 'last_error_reason' => 'Gateway timeout', ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.cp.ai-biography.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Admin/AiBiography') ->where('stats.total_records', 2) ->where('stats.needs_review', 1) ->where('stats.failed', 1) ->where('records.data.0.user.username', 'bioadmin') ->where('records.data.0.status', CreatorAiBiography::STATUS_FAILED) ->where('records.data.1.status', CreatorAiBiography::STATUS_NEEDS_REVIEW) ->where('endpoints.rebuildPattern', route('admin.cp.ai-biography.rebuild', ['user' => '__USER__']))); }); it('allows controlpanel-only admins to open the ai biography admin page', function (): void { $admin = aiBiographyAdminUser(); $this->actingAs($admin, 'controlpanel') ->get(route('cp.ai-biography.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Moderation/AiBiographyAdmin') ->where('title', 'AI Biography Review')); }); it('registers the ai biography entry in the cpad artworks menu', function (): void { $artworksPlugin = file_get_contents(base_path('packages/klevze/Plugins/Artworks/ServiceProvider.php')); $adminLayout = file_get_contents(base_path('resources/js/Layouts/AdminLayout.jsx')); expect($artworksPlugin)->toContain('admin.cp.ai-biography.index') ->and($adminLayout)->toContain("/moderation/ai-biography"); }); it('rebuilds an existing active biography through the admin surface', function (): void { $admin = aiBiographyAdminUser(); $creator = User::factory()->create(['username' => 'rebuildme']); biographyRecord($creator); for ($i = 0; $i < 5; $i++) { Artwork::factory()->for($creator)->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDays($i + 1), 'deleted_at' => null, ]); } $generator = Mockery::mock(AiBiographyGenerator::class); $generator->shouldReceive('generate') ->once() ->andReturn([ 'success' => true, 'text' => 'This refreshed biography gives the admin panel enough verified text to create a fresh active record while keeping the assertions here stable and predictable for the test suite.', 'action' => 'generated', 'errors' => [], 'model' => 'test-model', 'prompt_version' => 'v1.1', 'was_retried' => false, ]); app()->instance(AiBiographyService::class, new AiBiographyService(new AiBiographyInputBuilder(), $generator)); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.ai-biography.rebuild', ['user' => $creator->id])) ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('message', 'Biography rebuild completed.'); expect(CreatorAiBiography::query()->where('user_id', $creator->id)->where('is_active', true)->count())->toBe(1) ->and(CreatorAiBiography::query()->where('user_id', $creator->id)->count())->toBe(2); }); it('allows admins to approve flag and toggle visibility on biography records', function (): void { $admin = aiBiographyAdminUser(); $creator = User::factory()->create(['username' => 'reviewstate']); $record = biographyRecord($creator, [ 'needs_review' => true, 'status' => CreatorAiBiography::STATUS_NEEDS_REVIEW, ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.ai-biography.approve', ['biography' => $record->id])) ->assertOk() ->assertJsonPath('success', true); expect($record->fresh()->needs_review)->toBeFalse() ->and($record->fresh()->status)->toBe(CreatorAiBiography::STATUS_APPROVED); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.ai-biography.hide', ['biography' => $record->id])) ->assertOk() ->assertJsonPath('success', true); expect($record->fresh()->is_hidden)->toBeTrue(); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.ai-biography.show', ['biography' => $record->id])) ->assertOk() ->assertJsonPath('success', true); expect($record->fresh()->is_hidden)->toBeFalse(); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.ai-biography.flag', ['biography' => $record->id])) ->assertOk() ->assertJsonPath('success', true); expect($record->fresh()->needs_review)->toBeTrue() ->and($record->fresh()->status)->toBe(CreatorAiBiography::STATUS_NEEDS_REVIEW); });