From 5fd0fbe17ed230e22af602379fc4e41980260a13 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 26 Oct 2026 23:17:41 +0100 Subject: [PATCH 1/3] feat: tell followers about someone's first commit A first commit that gets an answer brings its author back far more often (61% against 40%), and newcomers already have followers, mostly brought over from GitHub, who never chose to follow them here. Their followers now get a notification for their first approved top-level commit, once, never from or to bots, and not across mutes or blocks. --- KNOWLEDGE_BASE.md | 2 + app/Actions/CreateCommit.php | 2 + .../CreateFirstCommitNotifications.php | 71 +++++++++++ app/Actions/UpdateCommitStatuses.php | 2 + app/Enums/NotificationType.php | 1 + app/Queries/NotificationBuilder.php | 2 +- resources/js/pages/notification/index.tsx | 6 +- resources/js/types/notification.ts | 2 +- tests/Unit/Actions/CreateCommitTest.php | 14 ++ .../CreateFirstCommitNotificationsTest.php | 120 ++++++++++++++++++ .../Unit/Actions/UpdateCommitStatusesTest.php | 11 ++ tests/Unit/Enums/NotificationTypeTest.php | 1 + 12 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 app/Actions/CreateFirstCommitNotifications.php create mode 100644 tests/Unit/Actions/CreateFirstCommitNotificationsTest.php diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 3f0a12f44..29ab470ed 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -322,6 +322,8 @@ Sources: [Pastéis setting](resources/js/components/pasteis-mode-setting.tsx), [ Open **Notifications** to read in-app activity: follows, replies, quotes, mentions, stars, and moderation messages about rejected commits (approvals do not notify). The navigation shows unread activity and checks for more every 30 seconds. While you have unread notifications or messages, the browser tab's icon shows an orange dot; a tab in the background checks about every five minutes. Related activity is grouped, with expandable details: stars per commit, follows per day, and replies and mentions per thread. Filters (Replies, Quotes, Stars, Mentions, Follows, Moderation) are remembered, and the unread count follows them. Opening the inbox marks all notifications read. You can follow back or unfollow from follower notifications. +When someone you follow makes their first commit, you get a notification for it under **All**, so a newcomer's first post gets seen and answered. It comes once per person, after approval, and never for replies. + Level-up notifications announce your new Unix rank under **All**, rather than one of the activity filters. Each level can notify you only once, even if you later lose and regain enough XP to reach it. On phones, **Replies**, **Quotes**, and **Stars** share one row with a **More notification filters** (`…`) button for **Mentions**, **Follows**, and **Moderation**. The menu stays open while you toggle filters so you can change several at once. diff --git a/app/Actions/CreateCommit.php b/app/Actions/CreateCommit.php index 95354559f..313e61358 100644 --- a/app/Actions/CreateCommit.php +++ b/app/Actions/CreateCommit.php @@ -29,6 +29,7 @@ public function __construct( private CreateReplyNotifications $createReplyNotifications, private CreateQuoteNotifications $createQuoteNotifications, private CreateMentionNotifications $createMentionNotifications, + private CreateFirstCommitNotifications $createFirstCommitNotifications, private CreateCommitImages $createCommitImages, private CreateWorldReply $createWorldReply, private UpdateCommitHotScores $updateCommitHotScores, @@ -85,6 +86,7 @@ public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $this->createReplyNotifications->handle($commit); $this->createQuoteNotifications->handle($commit); $this->createMentionNotifications->handle($commit); + $this->createFirstCommitNotifications->handle($commit); if ($linkPreview instanceof LinkPreview && $linkPreview->fetched_at === null) { dispatch(new UnfurlLink($linkPreview)); diff --git a/app/Actions/CreateFirstCommitNotifications.php b/app/Actions/CreateFirstCommitNotifications.php new file mode 100644 index 000000000..c4f9d791a --- /dev/null +++ b/app/Actions/CreateFirstCommitNotifications.php @@ -0,0 +1,71 @@ +whereKey(array_map(fn (Commit $commit): string => $commit->id, $commits)) + ->where('status', CommitStatus::Approved) + ->whereNull('parent_id') + ->whereHas('user', fn (Builder $user): Builder => $user->whereNotIn('github_id', User::BOT_GITHUB_IDS)) + // Deleted commits count too: whoever committed before isn't making their first commit. + ->whereNotExists(fn (QueryBuilder $earlier): QueryBuilder => $earlier + ->from('commits as earlier') + ->whereColumn('earlier.user_id', 'commits.user_id') + ->whereNull('earlier.parent_id') + ->where('earlier.status', CommitStatus::Approved) + ->whereColumn('earlier.created_at', '<', 'commits.created_at')) + ->with('user') + ->get(); + + $notifications = []; + + foreach ($firsts as $commit) { + $author = $commit->user; + assert($author instanceof User); + + $followerIds = $author->followers() + ->whereNotIn('users.github_id', User::BOT_GITHUB_IDS) + ->whereNotIn('users.id', $author->mutedBy()->newPivotQuery()->select('muter_id')) + ->whereNotIn('users.id', $author->idsAcrossBlocks()) + ->pluck('users.id'); + + foreach ($followerIds as $followerId) { + $notifications[] = [ + 'id' => Str::uuid7()->toString(), + 'recipient_id' => $followerId, + 'actor_id' => $author->id, + 'type' => NotificationType::FirstCommit->value, + 'commit_id' => $commit->id, + 'key' => 'first_commit:'.$author->id, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + } + + foreach (array_chunk($notifications, 500) as $chunk) { + Notification::query()->insertOrIgnore($chunk); + } + } +} diff --git a/app/Actions/UpdateCommitStatuses.php b/app/Actions/UpdateCommitStatuses.php index b8b901c3e..7b4887dbe 100644 --- a/app/Actions/UpdateCommitStatuses.php +++ b/app/Actions/UpdateCommitStatuses.php @@ -23,6 +23,7 @@ public function __construct( private CreateReplyNotifications $createReplyNotifications, private CreateQuoteNotifications $createQuoteNotifications, private CreateMentionNotifications $createMentionNotifications, + private CreateFirstCommitNotifications $createFirstCommitNotifications, private CreateWorldReply $createWorldReply, private UpdateCommitHotScores $updateCommitHotScores, private TomQuery $tomQuery, @@ -82,6 +83,7 @@ public function handle(array $commitIds, CommitStatus $status, ?string $reason = $this->createReplyNotifications->handle(...$commits); $this->createQuoteNotifications->handle(...$commits); $this->createMentionNotifications->handle(...$commits); + $this->createFirstCommitNotifications->handle(...$commits); $commits->each(fn (Commit $commit) => $this->createWorldReply->handle($commit)); diff --git a/app/Enums/NotificationType.php b/app/Enums/NotificationType.php index 88bb1943b..665df2a78 100644 --- a/app/Enums/NotificationType.php +++ b/app/Enums/NotificationType.php @@ -14,4 +14,5 @@ enum NotificationType: string case Quoted = 'quoted'; case Starred = 'starred'; case LeveledUp = 'leveled_up'; + case FirstCommit = 'first_commit'; } diff --git a/app/Queries/NotificationBuilder.php b/app/Queries/NotificationBuilder.php index b2abee21b..3f74ace7b 100644 --- a/app/Queries/NotificationBuilder.php +++ b/app/Queries/NotificationBuilder.php @@ -25,7 +25,7 @@ public function relevant(): self ->whereColumn('follows.follower_id', 'notifications.actor_id') ->whereColumn('follows.followed_id', 'notifications.recipient_id'))) ->orWhere(fn (Builder $query): Builder => $query - ->whereIn('type', [NotificationType::Replied, NotificationType::Quoted, NotificationType::Mentioned, NotificationType::Starred]) + ->whereIn('type', [NotificationType::Replied, NotificationType::Quoted, NotificationType::Mentioned, NotificationType::Starred, NotificationType::FirstCommit]) ->whereHas('commit', fn (CommitBuilder $commit): CommitBuilder => $commit ->where('status', CommitStatus::Approved))) ->orWhere(fn (Builder $query): Builder => $query diff --git a/resources/js/pages/notification/index.tsx b/resources/js/pages/notification/index.tsx index 8f934db0c..8a517d5ef 100644 --- a/resources/js/pages/notification/index.tsx +++ b/resources/js/pages/notification/index.tsx @@ -6,6 +6,7 @@ import { Check, ChevronDown, ChevronsUp, + GitCommitVertical, MessageSquareReply, Quote, Star, @@ -756,7 +757,8 @@ export function NotificationContent({ const isActorsCommit = notification.type === 'replied' || notification.type === 'quoted' || - notification.type === 'mentioned'; + notification.type === 'mentioned' || + notification.type === 'first_commit'; const level = notification.type === 'leveled_up' ? notification : null; const Icon = { followed: UserPlus, @@ -765,6 +767,7 @@ export function NotificationContent({ quoted: Quote, starred: Star, mentioned: AtSign, + first_commit: GitCommitVertical, approved: Check, rejected: X, }[notification.type]; @@ -804,6 +807,7 @@ export function NotificationContent({ ? `starred your ${starredImages.length === 1 ? 'image' : 'images'}` : 'starred your commit', mentioned: 'mentioned you', + first_commit: 'made their first commit', approved: 'Your commit was approved', rejected: 'Your commit was rejected', leveled_up: level && `You reached level ${level.level}: ${level.rank}`, diff --git a/resources/js/types/notification.ts b/resources/js/types/notification.ts index afb74b230..74c2256d7 100644 --- a/resources/js/types/notification.ts +++ b/resources/js/types/notification.ts @@ -54,7 +54,7 @@ export type Notification = }; }) | (BaseNotification & { - type: 'mentioned' | 'approved' | 'rejected'; + type: 'mentioned' | 'first_commit' | 'approved' | 'rejected'; commit_id: string; subject_id: null; commit: Omit; diff --git a/tests/Unit/Actions/CreateCommitTest.php b/tests/Unit/Actions/CreateCommitTest.php index 7ed3b12bf..b6f119c46 100644 --- a/tests/Unit/Actions/CreateCommitTest.php +++ b/tests/Unit/Actions/CreateCommitTest.php @@ -111,6 +111,20 @@ expect($parent->user->notifications()->sole()->commit_id)->toBe($reply->id); }); +it("tells the author's followers about their first commit", function (): void { + config(['app.admins' => ['nunomaduro']]); + + $author = User::factory()->create(['handle' => 'nunomaduro']); + $follower = User::factory()->create(); + $author->followers()->attach($follower); + + $commit = resolve(CreateCommit::class)->handle($author, 'Hello world!'); + + expect($follower->notifications()->sole()) + ->type->toBe(NotificationType::FirstCommit) + ->commit_id->toBe($commit->id); +}); + it('does not notify about pending replies', function (): void { $parent = Commit::factory()->create(); diff --git a/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php new file mode 100644 index 000000000..cd980350c --- /dev/null +++ b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php @@ -0,0 +1,120 @@ +create(); + $followers = User::factory()->count(2)->create(); + $author->followers()->attach($followers); + $commit = Commit::factory()->for($author)->create(); + + resolve(CreateFirstCommitNotifications::class)->handle($commit); + + foreach ($followers as $follower) { + $notification = $follower->notifications()->sole(); + + expect($notification->type)->toBe(NotificationType::FirstCommit) + ->and($notification->actor_id)->toBe($author->id) + ->and($notification->commit_id)->toBe($commit->id); + } +}); + +it('only notifies about the first commit', function (): void { + $author = User::factory()->create(); + $author->followers()->attach(User::factory()->create()); + Commit::factory()->for($author)->create(['created_at' => now()->subDay()]); + $second = Commit::factory()->for($author)->create(); + + resolve(CreateFirstCommitNotifications::class)->handle($second); + + expect(Notification::query()->count())->toBe(0); +}); + +it('counts a first commit that was deleted since', function (): void { + $author = User::factory()->create(); + $author->followers()->attach(User::factory()->create()); + Commit::factory()->for($author)->create(['created_at' => now()->subDay(), 'deleted_at' => now()]); + $second = Commit::factory()->for($author)->create(); + + resolve(CreateFirstCommitNotifications::class)->handle($second); + + expect(Notification::query()->count())->toBe(0); +}); + +it('waits for the first commit to be approved', function (): void { + $author = User::factory()->create(); + $author->followers()->attach(User::factory()->create()); + + resolve(CreateFirstCommitNotifications::class)->handle( + Commit::factory()->for($author)->pending()->create(), + Commit::factory()->for($author)->rejected()->create(), + ); + + expect(Notification::query()->count())->toBe(0); +}); + +it('notifies about the first commit approved after a rejected one', function (): void { + $author = User::factory()->create(); + $follower = User::factory()->create(); + $author->followers()->attach($follower); + Commit::factory()->for($author)->rejected()->create(['created_at' => now()->subDay()]); + $commit = Commit::factory()->for($author)->create(); + + resolve(CreateFirstCommitNotifications::class)->handle($commit); + + expect($follower->notifications()->sole()->commit_id)->toBe($commit->id); +}); + +it('does not count replies as a first commit', function (): void { + $author = User::factory()->create(); + $author->followers()->attach(User::factory()->create()); + $reply = Commit::factory()->for($author)->for(Commit::factory(), 'parent')->create(); + + resolve(CreateFirstCommitNotifications::class)->handle($reply); + + expect(Notification::query()->count())->toBe(0); +}); + +it('notifies only once, about the earliest of first commits approved together', function (): void { + $author = User::factory()->create(); + $follower = User::factory()->create(); + $author->followers()->attach($follower); + $first = Commit::factory()->for($author)->create(['created_at' => now()->subMinute()]); + $second = Commit::factory()->for($author)->create(); + + $action = resolve(CreateFirstCommitNotifications::class); + $action->handle($second, $first); + $action->handle($first); + + expect($follower->notifications()->sole()->commit_id)->toBe($first->id); +}); + +it('leaves out bots, and followers who mute the author or are across a block', function (): void { + $author = User::factory()->create(); + resolve(CreateTomFriendship::class)->handle($author); + [$follower, $muter, $blocker, $blocked] = User::factory()->count(4)->create()->all(); + $author->followers()->attach([$follower->id, $muter->id, $blocker->id, $blocked->id]); + $muter->muting()->attach($author); + $blocker->blocking()->attach($author); + $author->blocking()->attach($blocked); + + resolve(CreateFirstCommitNotifications::class)->handle(Commit::factory()->for($author)->create()); + + expect(Notification::query()->pluck('recipient_id')->all())->toBe([$follower->id]); +}); + +it('does not notify about the first commit of a bot', function (): void { + $author = User::factory()->create(['github_id' => CreateTomFriendship::GITHUB_ID]); + $author->followers()->attach(User::factory()->create()); + + resolve(CreateFirstCommitNotifications::class)->handle(Commit::factory()->for($author)->create()); + + expect(Notification::query()->count())->toBe(0); +}); diff --git a/tests/Unit/Actions/UpdateCommitStatusesTest.php b/tests/Unit/Actions/UpdateCommitStatusesTest.php index 70519300e..67fc69ae9 100644 --- a/tests/Unit/Actions/UpdateCommitStatusesTest.php +++ b/tests/Unit/Actions/UpdateCommitStatusesTest.php @@ -97,6 +97,17 @@ ->toEqualCanonicalizing($quotes->modelKeys()); }); +it("tells the author's followers about their first commit once it is approved", function (): void { + $author = User::factory()->create(); + $follower = User::factory()->create(); + $author->followers()->attach($follower); + $commit = Commit::factory()->for($author)->pending()->create(); + + resolve(UpdateCommitStatuses::class)->handle([$commit->id], CommitStatus::Approved); + + expect($follower->notifications()->sole()->commit_id)->toBe($commit->id); +}); + it('lifts the thread when a reply is approved', function (): void { $commit = Commit::factory()->create(); $reply = Commit::factory()->pending()->for($commit, 'parent')->create(); diff --git a/tests/Unit/Enums/NotificationTypeTest.php b/tests/Unit/Enums/NotificationTypeTest.php index 5aced3bec..864490e37 100644 --- a/tests/Unit/Enums/NotificationTypeTest.php +++ b/tests/Unit/Enums/NotificationTypeTest.php @@ -14,5 +14,6 @@ 'Quoted' => 'quoted', 'Starred' => 'starred', 'LeveledUp' => 'leveled_up', + 'FirstCommit' => 'first_commit', ]); }); From 2d79cc9d560ac2bd122c1445c9c4734ae24ba92e Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 26 Oct 2026 23:22:25 +0100 Subject: [PATCH 2/3] fix: leave out the followers a first commit mentions or quotes They already hear about it as a mention or a quote. --- KNOWLEDGE_BASE.md | 4 ++-- app/Actions/CreateFirstCommitNotifications.php | 8 +++++--- .../Actions/CreateFirstCommitNotificationsTest.php | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 29ab470ed..ed54b4d18 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -320,7 +320,7 @@ Sources: [Pastéis setting](resources/js/components/pasteis-mode-setting.tsx), [ ### Notifications -Open **Notifications** to read in-app activity: follows, replies, quotes, mentions, stars, and moderation messages about rejected commits (approvals do not notify). The navigation shows unread activity and checks for more every 30 seconds. While you have unread notifications or messages, the browser tab's icon shows an orange dot; a tab in the background checks about every five minutes. Related activity is grouped, with expandable details: stars per commit, follows per day, and replies and mentions per thread. Filters (Replies, Quotes, Stars, Mentions, Follows, Moderation) are remembered, and the unread count follows them. Opening the inbox marks all notifications read. You can follow back or unfollow from follower notifications. +Open **Notifications** to read in-app activity: follows, replies, quotes, mentions, stars, first commits of people you follow, and moderation messages about rejected commits (approvals do not notify). The navigation shows unread activity and checks for more every 30 seconds. While you have unread notifications or messages, the browser tab's icon shows an orange dot; a tab in the background checks about every five minutes. Related activity is grouped, with expandable details: stars per commit, follows per day, and replies and mentions per thread. Filters (Replies, Quotes, Stars, Mentions, Follows, Moderation) are remembered, and the unread count follows them. Opening the inbox marks all notifications read. You can follow back or unfollow from follower notifications. When someone you follow makes their first commit, you get a notification for it under **All**, so a newcomer's first post gets seen and answered. It comes once per person, after approval, and never for replies. @@ -330,7 +330,7 @@ On phones, **Replies**, **Quotes**, and **Stars** share one row with a **More no Pending replies, quotes, and mentions wait for approval before notifying others. Self-actions do not notify you; mentioning the parent or quoted author does not duplicate their reply or quote notification. A new follower notifies only once even if they unfollow and follow again. Deleted or unavailable sources, mutes, and blocks affect which notifications remain visible. Rejected commits can produce a moderation message with a reason, without revealing a moderator's identity. -Sources: [notifications page](resources/js/pages/notification/index.tsx), [notification filtering](app/Queries/NotificationBuilder.php), [grouping](app/Queries/NotificationGroupsQuery.php), [moderation notifications](app/Actions/UpdateCommitStatuses.php). +Sources: [notifications page](resources/js/pages/notification/index.tsx), [notification filtering](app/Queries/NotificationBuilder.php), [grouping](app/Queries/NotificationGroupsQuery.php), [moderation notifications](app/Actions/UpdateCommitStatuses.php), [first commit notifications](app/Actions/CreateFirstCommitNotifications.php). Remotes alerts are controlled under **Settings → Profile → Remotes**. **On-site notifications** and **Notification sound** are on by default. On-site alerts stay inside /dev/null and never ask for browser permission. Sound does not require notification permission and plays only while the site is running; the browser may require interaction before it can play. diff --git a/app/Actions/CreateFirstCommitNotifications.php b/app/Actions/CreateFirstCommitNotifications.php index c4f9d791a..a1217eaaa 100644 --- a/app/Actions/CreateFirstCommitNotifications.php +++ b/app/Actions/CreateFirstCommitNotifications.php @@ -18,8 +18,9 @@ /** * Tells the author's followers about their first commit, so it gets an answer: a newcomer's followers mostly * came from GitHub, and never chose to follow them here. Only once the commit is approved, and only for the - * author's first top-level commit that was ever approved. Never from or to bots, and nobody hears from people - * they mute or from across a block. Safe to call more than once. + * author's first top-level commit that was ever approved. Never from or to bots, nor to the people it mentions + * or quotes, who already hear about it. Nobody hears from people they mute or from across a block. Safe to call + * more than once. */ public function handle(Commit ...$commits): void { @@ -35,7 +36,7 @@ public function handle(Commit ...$commits): void ->whereNull('earlier.parent_id') ->where('earlier.status', CommitStatus::Approved) ->whereColumn('earlier.created_at', '<', 'commits.created_at')) - ->with('user') + ->with(['user', 'quotedCommit']) ->get(); $notifications = []; @@ -48,6 +49,7 @@ public function handle(Commit ...$commits): void ->whereNotIn('users.github_id', User::BOT_GITHUB_IDS) ->whereNotIn('users.id', $author->mutedBy()->newPivotQuery()->select('muter_id')) ->whereNotIn('users.id', $author->idsAcrossBlocks()) + ->whereNotIn('users.id', [...array_values($commit->mentions ?? []), ...array_filter([$commit->quotedCommit?->user_id])]) ->pluck('users.id'); foreach ($followerIds as $followerId) { diff --git a/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php index cd980350c..24ec4b173 100644 --- a/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php +++ b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php @@ -118,3 +118,17 @@ expect(Notification::query()->count())->toBe(0); }); + +it('leaves out followers it mentions or quotes, who already hear about it', function (): void { + $author = User::factory()->create(); + [$follower, $mentioned, $quoted] = User::factory()->count(3)->create()->all(); + $author->followers()->attach([$follower->id, $mentioned->id, $quoted->id]); + $commit = Commit::factory()->for($author)->create([ + 'mentions' => [$mentioned->handle => $mentioned->id], + 'quoted_commit_id' => Commit::factory()->for($quoted)->create()->id, + ]); + + resolve(CreateFirstCommitNotifications::class)->handle($commit); + + expect(Notification::query()->pluck('recipient_id')->all())->toBe([$follower->id]); +}); From 6da70ec6d65a2c66b4d08be7d0139db3e3a2dc18 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 26 Oct 2026 23:29:06 +0100 Subject: [PATCH 3/3] perf: look nothing up for replies when checking for a first commit Replies are never a first commit, and most of what gets committed. --- app/Actions/CreateFirstCommitNotifications.php | 12 +++++++++++- .../Actions/CreateFirstCommitNotificationsTest.php | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/Actions/CreateFirstCommitNotifications.php b/app/Actions/CreateFirstCommitNotifications.php index a1217eaaa..c62ce824a 100644 --- a/app/Actions/CreateFirstCommitNotifications.php +++ b/app/Actions/CreateFirstCommitNotifications.php @@ -24,8 +24,18 @@ */ public function handle(Commit ...$commits): void { + // A reply is never a first commit, so replies, most of what gets committed, cost no query. + $topLevelIds = array_values(array_map( + fn (Commit $commit): string => $commit->id, + array_filter($commits, fn (Commit $commit): bool => $commit->parent_id === null), + )); + + if ($topLevelIds === []) { + return; + } + $firsts = Commit::query() - ->whereKey(array_map(fn (Commit $commit): string => $commit->id, $commits)) + ->whereKey($topLevelIds) ->where('status', CommitStatus::Approved) ->whereNull('parent_id') ->whereHas('user', fn (Builder $user): Builder => $user->whereNotIn('github_id', User::BOT_GITHUB_IDS)) diff --git a/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php index 24ec4b173..abe718299 100644 --- a/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php +++ b/tests/Unit/Actions/CreateFirstCommitNotificationsTest.php @@ -8,6 +8,7 @@ use App\Models\Commit; use App\Models\Notification; use App\Models\User; +use Illuminate\Support\Facades\DB; it("notifies the author's followers about their first commit", function (): void { $author = User::factory()->create(); @@ -82,6 +83,15 @@ expect(Notification::query()->count())->toBe(0); }); +it('looks nothing up for replies', function (): void { + $reply = Commit::factory()->for(Commit::factory(), 'parent')->create(); + + DB::enableQueryLog(); + resolve(CreateFirstCommitNotifications::class)->handle($reply); + + expect(DB::getQueryLog())->toBeEmpty(); +}); + it('notifies only once, about the earliest of first commits approved together', function (): void { $author = User::factory()->create(); $follower = User::factory()->create();