Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use App\Enums\LivePlatform;
use App\Enums\Log;
use App\Enums\RemotesFrom;
use App\Enums\UnixRank;
use App\Queries\UserAchievementsQuery;
use Carbon\CarbonInterface;
use Database\Factories\UserFactory;
use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract;
Expand Down Expand Up @@ -252,13 +254,14 @@ public function commits(): HasMany
}

/**
* Ten followers are enough, so the check stops at the tenth instead of counting every follower.
* Requires daemon-level public XP. Stop at the tenth follower instead of counting every follower.
*/
public function hasGoodReputation(): bool
{
return $this->created_at->lt(now()->subHour())
&& $this->commits()->approved()->exists()
&& $this->followers()->skip(9)->exists();
&& $this->followers()->skip(9)->exists()
&& resolve(UserAchievementsQuery::class)->get($this, null)['xp'] >= UnixRank::Daemon->minXp();
}

/**
Expand Down
41 changes: 39 additions & 2 deletions tests/Unit/Actions/CreateCommitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Models\LinkPreview;
use App\Models\Notification;
use App\Models\User;
use App\Queries\UserAchievementsQuery;
use App\Services\TheAlgorithm;
use Illuminate\Database\QueryException;
use Illuminate\Http\UploadedFile;
Expand Down Expand Up @@ -68,10 +69,10 @@
followersOf($user, $followers);

if ($previousStatus instanceof CommitStatus) {
$previous = Commit::factory()->for($user)->create(['status' => $previousStatus]);
$previous = Commit::factory()->for($user)->count(4)->create(['status' => $previousStatus]);

if ($deleted) {
$previous->delete();
$previous->each->delete();
}
}

Expand Down Expand Up @@ -99,6 +100,41 @@
'deleted approved history' => [3601, 10, CommitStatus::Approved, true, false],
]);

it('requires at least daemon public xp to skip ai review', function (int $commits, int $starsGiven, int $starsReceived, int $xp, bool $approved): void {
config(['services.openai.key' => 'sk-test']);
Queue::fake();

$user = User::factory()->create(['created_at' => now()->subHours(2)]);
followersOf($user, 10);
$history = Commit::factory()->for($user)->count($commits)->create();
$user->starredCommits()->attach(Commit::factory()->count($starsGiven)->create()->modelKeys());
foreach (User::factory()->count($starsReceived)->create() as $stargazer) {
$stargazer->starredCommits()->attach($history->first());
}

// Private activity must not boost moderation eligibility, even when the queued job rechecks it.
Commit::factory()->for($user)->pending()->count(5)->create();
Commit::factory()->for($user)->rejected()->count(5)->create();
$deleted = Commit::factory()->for($user)->create();
$deleted->delete();

expect(resolve(UserAchievementsQuery::class)->get($user, null)['xp'])->toBe($xp);

$commit = resolve(CreateCommit::class)->handle($user, 'A new commit');

expect($commit->status)->toBe($approved ? CommitStatus::Approved : CommitStatus::Pending);

if ($approved) {
Queue::assertNotPushed(ModerateCommit::class);
} else {
Queue::assertPushed(ModerateCommit::class);
}
})->with([
'below daemon' => [3, 2, 0, 49, false],
'exactly daemon' => [3, 0, 1, 50, true],
'higher rank' => [14, 0, 0, 155, true],
]);

it('notifies the parent author about replies of admins', function (): void {
config(['app.admins' => ['nunomaduro']]);

Expand Down Expand Up @@ -476,6 +512,7 @@
$reported = Commit::factory()->for($user)->create();
if ($reputable) {
followersOf($user, 10);
Commit::factory()->for($user)->count(3)->create();
}

CommitReport::factory()->for($reported)->create();
Expand Down
27 changes: 21 additions & 6 deletions tests/Unit/Jobs/ModerateCommitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,22 +373,36 @@ function reviewResponses(array $responses = []): Closure
expect($commit->refresh()->ai_decision)->toBeNull();
});

it('approves queued commits without calling the ai when the author has good reputation', function (): void {
Http::fake();
it('only skips ai review for queued commits with daemon public xp', function (bool $daemon): void {
Http::fake([
'api.openai.com/v1/moderations' => Http::response(moderationResponse()),
'api.openai.com/v1/responses' => reviewResponses(),
]);

$user = User::factory()->create(['created_at' => now()->subHour()->subSecond()]);
Commit::factory()->for($user)->create();
$history = Commit::factory()->for($user)->count(3)->create();
if ($daemon) {
User::factory()->create()->starredCommits()->attach($history->first());
} else {
$user->starredCommits()->attach(Commit::factory()->count(2)->create()->modelKeys());
}

Commit::factory()->for($user)->rejected()->count(5)->create();
$commit = Commit::factory()->for($user)->pending()->create();
followersOf($user, 10);

dispatch_sync(new ModerateCommit($commit));

expect($commit->refresh())
->status->toBe(CommitStatus::Approved)
->ai_decision->toBeNull();
->ai_decision->toBe($daemon ? null : CommitStatus::Approved);

Http::assertNothingSent();
});
if ($daemon) {
Http::assertNothingSent();
} else {
Http::assertSentCount(2);
}
})->with([false, true]);

it('skips commits that were deleted in the meantime', function (): void {
Http::fake();
Expand Down Expand Up @@ -469,6 +483,7 @@ function reviewResponses(array $responses = []): Closure
$user = User::factory()->create(['created_at' => now()->subHours(2)]);
$reported = Commit::factory()->for($user)->create();
followersOf($user, 10);
Commit::factory()->for($user)->count(3)->create();
$commit = Commit::factory()->for($user)->pending()->create();
CommitReport::factory()->for($reported)->create();

Expand Down