From 42d210a13ccc66583090b625a4e2794dd796478d Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 19 May 2026 17:25:33 +0200 Subject: [PATCH 01/17] Handle Carbon 3 timestamp parsing and add coverage for timestamp handling Bug: T426592 --- app/Helper/MWTimestampHelper.php | 9 +++++++-- tests/Helper/MWTimestampHelperTest.php | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index a5d03f71e..bab92e404 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -15,8 +15,13 @@ class MWTimestampHelper { private const MWTimestampFormat = 'YmdHis'; public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmutable { - $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); - if ($carbon === null) { + try { + $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); + } catch (InvalidFormatException $exception) { + throw new InvalidFormatException('Unable to create Carbon object', 0, $exception); + } + + if (!$carbon instanceof CarbonImmutable) { throw new InvalidFormatException('Unable to create Carbon object'); } diff --git a/tests/Helper/MWTimestampHelperTest.php b/tests/Helper/MWTimestampHelperTest.php index ed054cdb4..a82d81f9d 100644 --- a/tests/Helper/MWTimestampHelperTest.php +++ b/tests/Helper/MWTimestampHelperTest.php @@ -19,6 +19,7 @@ public function testGetCarbonFromMWTimestamp() { public function testGetCarbonFromMWTimestampWithInvalidTimestamp() { $this->expectException(InvalidFormatException::class); + $this->expectExceptionMessage('Unable to create Carbon object'); $invalidMwTimestamp = 'invalid_timestamp'; MWTimestampHelper::getCarbonFromMWTimestamp($invalidMwTimestamp); From a7034a0acb0547cdac327e16acd1aaab75111fb5 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 19:55:48 +0200 Subject: [PATCH 02/17] Add test case to cover wiki noti threshold boundary --- .../SendEmptyWikiNotificationsJobTest.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php index 7fb09ba36..dd07b544c 100644 --- a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php +++ b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php @@ -38,7 +38,7 @@ public function testEmptyWikiNotificationsSendNotification() { Notification::fake(); $user = User::factory()->create(['verified' => true]); $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); - $manager = WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); $job = new SendEmptyWikiNotificationsJob; @@ -50,6 +50,24 @@ public function testEmptyWikiNotificationsSendNotification() { ); } + // empty wikis, that are almost old enough (29 days and 23 hrs) + public function testEmptyWikiNotificationsNotSendNotification() { + $thresholdDaysAgo = Carbon::now() + ->subDays((config('wbstack.wiki_empty_notification_threshold') - 1)) + ->subHours(23) + ->toDateTimeString(); + + Notification::fake(); + $user = User::factory()->create(['verified' => true]); + $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); + + $job = new SendEmptyWikiNotificationsJob; + $this->assertFalse($job->checkIfWikiIsOldAndEmpty($wiki)); + $job->handle(); + } + // fresh wiki that does not have lifecycle event records yet public function testEmptyWikiNotificationsFreshWiki() { $now = Carbon::now()->toDateTimeString(); From b6d71401e91a103401784d32e4a2c63f58cd1bf3 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:44:28 +0200 Subject: [PATCH 03/17] Add test second-precision lastEdit threshold in platform stats summary --- tests/Jobs/PlatformStatsSummaryJobTest.php | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Jobs/PlatformStatsSummaryJobTest.php b/tests/Jobs/PlatformStatsSummaryJobTest.php index 4a954200f..d94e970dc 100644 --- a/tests/Jobs/PlatformStatsSummaryJobTest.php +++ b/tests/Jobs/PlatformStatsSummaryJobTest.php @@ -274,4 +274,50 @@ public function testCreationStats() { ); } + + public function testPrepareStatsTreatsSecondPrecisionTimestampAtThresholdAsActive() { + $currentTime = CarbonImmutable::now(); + + $wiki = Wiki::factory()->create(['deleted_at' => null, 'domain' => 'thresholdtest.com']); + WikiDb::create([ + 'name' => 'mwdb_threshold_' . $wiki->id, + 'user' => 'user', + 'password' => 'password', + 'version' => 'version', + 'prefix' => 'prefix', + 'wiki_id' => $wiki->id, + ]); + + Http::fake([ + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=122&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=120&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + ]); + + $job = new PlatformStatsSummaryJob; + (function ($resolver): void { + $this->mwHostResolver = $resolver; + })->call($job, $this->mockMwHostResolver); + + $groups = $job->prepareStats([ + [ + 'wiki' => 'thresholdtest.com', + 'edits' => 1, + 'pages' => 1, + 'users' => 1, + 'active_users' => 1, + 'lastEdit' => MWTimestampHelper::getMWTimestampFromCarbon( + $currentTime->subSeconds(config('wbstack.platform_summary_inactive_threshold')) + ), + 'first100UsingOauth' => '0', + 'platform_summary_version' => 'v1', + ], + ], [$wiki]); + + $this->assertSame(1, $groups['edited_last_90_days']); + $this->assertSame(0, $groups['not_edited_last_90_days']); + } } From 1bc2dbf19a8b2e8fe97390f83202ab89595a2786 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:51:40 +0200 Subject: [PATCH 04/17] Add test for fractional day diffs in conversion metrics --- tests/Routes/Wiki/ConversionMetricTest.php | 41 +++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index a1c479401..800d41c83 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -58,10 +58,10 @@ private function createTestWiki($name, $createdWeeksAgo, $firstEditedWeeksAgo, $ $wiki->created_at = $current_date->subWeeks($createdWeeksAgo); $events = $wiki->wikiLifecycleEvents(); $update = []; - if ($lastEditedWeeksAgo) { + if ($lastEditedWeeksAgo !== null) { $update['last_edited'] = $current_date->subWeeks($lastEditedWeeksAgo); } - if ($firstEditedWeeksAgo) { + if ($firstEditedWeeksAgo !== null) { $update['first_edited'] = $current_date->subWeeks($firstEditedWeeksAgo); } $events->updateOrCreate($update); @@ -118,7 +118,7 @@ public function testDownloadJson() { ); $response->assertJsonFragment( [ - 'domain' => 'unused.for.a.year.but.now.active.wikibase.cloud', + 'domain' => 'acvtively.used.for.the.last.year.wikibase.cloud', 'time_to_engage_days' => 0, 'time_before_wiki_abandoned_days' => null, 'number_of_active_editors' => 5, @@ -134,9 +134,42 @@ public function testDownloadJson() { ); } + public function testDownloadJsonTruncatesFractionalDayDiffs() { + $currentDate = CarbonImmutable::now(); + $createdAt = $currentDate->subDays(200)->subHours(12); //200.5 days ago + $firstEditedAt = $createdAt->addDays(1)->addHours(12); //1.5 days after + $lastEditedAt = $currentDate->subDays(100); //100 days ago + + $wiki = Wiki::factory()->create([ + 'domain' => 'fractional.days.cloud', + 'sitename' => 'Fractional Days Site', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $wiki->id, + 'pages' => 77, + 'activeusers' => 2, + ]); + $wiki->created_at = $createdAt; + $wiki->wikiLifecycleEvents()->updateOrCreate([ + 'first_edited' => $firstEditedAt, + 'last_edited' => $lastEditedAt, + ]); + $wiki->save(); + + $response = $this->getJson($this->route); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'domain' => 'fractional.days.cloud', + 'time_to_engage_days' => 1, + 'time_before_wiki_abandoned_days' => 100, + 'number_of_active_editors' => 2, + ]); + } + public function testFunctionalWithMissingLifecycleEventsandStats() { $wiki = Wiki::factory()->create([ - 'domain' => 'very.new.wikibase.cloud', 'sitename' => 'bsite', + 'domain' => 'very.new.cloud', 'sitename' => 'Very New Site', ]); $response = $this->get($this->route); From 38ca74c774ef83fd32d906972b45940ecd530789 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:58:41 +0200 Subject: [PATCH 05/17] fix linting error --- tests/Routes/Wiki/ConversionMetricTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index 800d41c83..9188bdb9f 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -136,9 +136,9 @@ public function testDownloadJson() { public function testDownloadJsonTruncatesFractionalDayDiffs() { $currentDate = CarbonImmutable::now(); - $createdAt = $currentDate->subDays(200)->subHours(12); //200.5 days ago - $firstEditedAt = $createdAt->addDays(1)->addHours(12); //1.5 days after - $lastEditedAt = $currentDate->subDays(100); //100 days ago + $createdAt = $currentDate->subDays(200)->subHours(12); // 200.5 days ago + $firstEditedAt = $createdAt->addDays(1)->addHours(12); // 1.5 days after + $lastEditedAt = $currentDate->subDays(100); // 100 days ago $wiki = Wiki::factory()->create([ 'domain' => 'fractional.days.cloud', From 7e26e94896e21a58a633abb8191037e01196eefa Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 21:01:56 +0200 Subject: [PATCH 06/17] remove unwanted changes --- bootstrap/providers.php | 17 +++++++++++++++++ tests/Routes/Wiki/ConversionMetricTest.php | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 bootstrap/providers.php diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 000000000..97b8e7ebd --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,17 @@ +assertJsonFragment( [ - 'domain' => 'acvtively.used.for.the.last.year.wikibase.cloud', + 'domain' => 'unused.for.a.year.but.now.active.wikibase.cloud', 'time_to_engage_days' => 0, 'time_before_wiki_abandoned_days' => null, 'number_of_active_editors' => 5, @@ -169,7 +169,7 @@ public function testDownloadJsonTruncatesFractionalDayDiffs() { public function testFunctionalWithMissingLifecycleEventsandStats() { $wiki = Wiki::factory()->create([ - 'domain' => 'very.new.cloud', 'sitename' => 'Very New Site', + 'domain' => 'very.new.wikibase.cloud', 'sitename' => 'bsite', ]); $response = $this->get($this->route); From 159acc376c5c84a58c608ecce8dd385945dd2af1 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 21:04:27 +0200 Subject: [PATCH 07/17] Remove accidental bootstrap/providers.php from branch --- bootstrap/providers.php | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 bootstrap/providers.php diff --git a/bootstrap/providers.php b/bootstrap/providers.php deleted file mode 100644 index 97b8e7ebd..000000000 --- a/bootstrap/providers.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Tue, 26 May 2026 16:15:45 +0200 Subject: [PATCH 08/17] Change exception message to be clearer --- app/Helper/MWTimestampHelper.php | 4 ++-- tests/Helper/MWTimestampHelperTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index bab92e404..7315d78a7 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -18,11 +18,11 @@ public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmu try { $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); } catch (InvalidFormatException $exception) { - throw new InvalidFormatException('Unable to create Carbon object', 0, $exception); + throw new InvalidFormatException('Unable to create Carbon object: invalid MW timestamp format', 0, $exception); } if (!$carbon instanceof CarbonImmutable) { - throw new InvalidFormatException('Unable to create Carbon object'); + throw new InvalidFormatException('Unable to create Carbon object: parser did not return CarbonImmutable'); } return $carbon; diff --git a/tests/Helper/MWTimestampHelperTest.php b/tests/Helper/MWTimestampHelperTest.php index a82d81f9d..5b04b5dcb 100644 --- a/tests/Helper/MWTimestampHelperTest.php +++ b/tests/Helper/MWTimestampHelperTest.php @@ -19,7 +19,7 @@ public function testGetCarbonFromMWTimestamp() { public function testGetCarbonFromMWTimestampWithInvalidTimestamp() { $this->expectException(InvalidFormatException::class); - $this->expectExceptionMessage('Unable to create Carbon object'); + $this->expectExceptionMessage('Unable to create Carbon object: invalid MW timestamp format'); $invalidMwTimestamp = 'invalid_timestamp'; MWTimestampHelper::getCarbonFromMWTimestamp($invalidMwTimestamp); From 854d69a6fca4f6d557165b1a47bf38b4fe83219e Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 26 May 2026 22:27:18 +0200 Subject: [PATCH 09/17] remove try/catch block --- app/Helper/MWTimestampHelper.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index 7315d78a7..15395c961 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -15,11 +15,7 @@ class MWTimestampHelper { private const MWTimestampFormat = 'YmdHis'; public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmutable { - try { - $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); - } catch (InvalidFormatException $exception) { - throw new InvalidFormatException('Unable to create Carbon object: invalid MW timestamp format', 0, $exception); - } + $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); if (!$carbon instanceof CarbonImmutable) { throw new InvalidFormatException('Unable to create Carbon object: parser did not return CarbonImmutable'); From 3d1a0bf5b9ab0b394ba7884003fc810a906ef1ef Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 26 May 2026 22:30:27 +0200 Subject: [PATCH 10/17] remove unnecessarry test method --- tests/Helper/MWTimestampHelperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Helper/MWTimestampHelperTest.php b/tests/Helper/MWTimestampHelperTest.php index 5b04b5dcb..ed054cdb4 100644 --- a/tests/Helper/MWTimestampHelperTest.php +++ b/tests/Helper/MWTimestampHelperTest.php @@ -19,7 +19,6 @@ public function testGetCarbonFromMWTimestamp() { public function testGetCarbonFromMWTimestampWithInvalidTimestamp() { $this->expectException(InvalidFormatException::class); - $this->expectExceptionMessage('Unable to create Carbon object: invalid MW timestamp format'); $invalidMwTimestamp = 'invalid_timestamp'; MWTimestampHelper::getCarbonFromMWTimestamp($invalidMwTimestamp); From 65ef80b2dc801f5a9c3d61621a6730932b0d2077 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 12:26:10 +0200 Subject: [PATCH 11/17] Remove if block --- app/Helper/MWTimestampHelper.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index 15395c961..6ce9f0ee2 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -9,7 +9,6 @@ namespace App\Helper; use Carbon\CarbonImmutable; -use Carbon\Exceptions\InvalidFormatException; class MWTimestampHelper { private const MWTimestampFormat = 'YmdHis'; @@ -17,10 +16,6 @@ class MWTimestampHelper { public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmutable { $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); - if (!$carbon instanceof CarbonImmutable) { - throw new InvalidFormatException('Unable to create Carbon object: parser did not return CarbonImmutable'); - } - return $carbon; } From 1d50bc18ba297c65958b95eb798364bb60e0144c Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 16:17:30 +0200 Subject: [PATCH 12/17] Add comments explaining the purpose of int cast --- app/Http/Controllers/ConversionMetricController.php | 3 +++ app/Jobs/PlatformStatsSummaryJob.php | 1 + app/Jobs/SendEmptyWikiNotificationsJob.php | 1 + 3 files changed, 5 insertions(+) diff --git a/app/Http/Controllers/ConversionMetricController.php b/app/Http/Controllers/ConversionMetricController.php index 2074fa201..687bf443e 100644 --- a/app/Http/Controllers/ConversionMetricController.php +++ b/app/Http/Controllers/ConversionMetricController.php @@ -34,13 +34,16 @@ public function index(Request $request) { $daysSinceLastEdit = null; if ($wikiLastEditedTime !== null) { + // Cast to int to avoid fractional-day values in CSV/JSON and keep cutoff logic predictable. $daysSinceLastEdit = (int) $wikiLastEditedTime->diffInDays($current_date, false); } if ($daysSinceLastEdit !== null && $daysSinceLastEdit >= 90) { + // Keep exported duration as whole days for stable reporting. $time_before_wiki_abandoned_days = (int) $wiki->created_at->diffInDays($wikiLastEditedTime, false); } if ($wikiFirstEditedTime !== null) { + // Keep exported duration as whole days for stable reporting. $time_to_engage_days = (int) $wiki->created_at->diffInDays($wikiFirstEditedTime, false); } $wiki_number_of_editors = $wiki->wikiSiteStats()->first()['activeusers'] ?? null; diff --git a/app/Jobs/PlatformStatsSummaryJob.php b/app/Jobs/PlatformStatsSummaryJob.php index 7bede2609..340422abd 100644 --- a/app/Jobs/PlatformStatsSummaryJob.php +++ b/app/Jobs/PlatformStatsSummaryJob.php @@ -132,6 +132,7 @@ public function prepareStats(array $allStats, $wikis): array { // is it edited in the last 90 days? if (!is_null($stats['lastEdit'])) { $lastTimestamp = MWTimestampHelper::getCarbonFromMWTimestamp(intval($stats['lastEdit'])); + // Carbon can return fractional seconds; cast to int so threshold checks use whole-second precision. $diff = (int) $lastTimestamp->diffInSeconds($currentTime, true); if ($diff <= $this->inactiveThreshold) { diff --git a/app/Jobs/SendEmptyWikiNotificationsJob.php b/app/Jobs/SendEmptyWikiNotificationsJob.php index 76a4e5d7d..d80e0e07d 100644 --- a/app/Jobs/SendEmptyWikiNotificationsJob.php +++ b/app/Jobs/SendEmptyWikiNotificationsJob.php @@ -34,6 +34,7 @@ public function checkIfWikiIsOldAndEmpty(Wiki $wiki) { $emptyDaysThreshold = config('wbstack.wiki_empty_notification_threshold'); $createdAt = $wiki->created_at; $now = CarbonImmutable::now(); + // Normalize to whole days so comparisons with the integer config threshold stay deterministic. $emptyWikiDays = (int) $createdAt->diffInDays($now, true); $firstEdited = $wiki->wikiLifecycleEvents->first_edited; From e5037b972c81e9afb66e3f0b9440fc1f806b6b9b Mon Sep 17 00:00:00 2001 From: Dat WMDE Date: Thu, 28 May 2026 16:00:08 +0200 Subject: [PATCH 13/17] Update comments based on suggestions Co-authored-by: Ollie <43674967+outdooracorn@users.noreply.github.com> --- app/Http/Controllers/ConversionMetricController.php | 6 +++--- app/Jobs/PlatformStatsSummaryJob.php | 3 ++- app/Jobs/SendEmptyWikiNotificationsJob.php | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/ConversionMetricController.php b/app/Http/Controllers/ConversionMetricController.php index 687bf443e..ab66f52de 100644 --- a/app/Http/Controllers/ConversionMetricController.php +++ b/app/Http/Controllers/ConversionMetricController.php @@ -34,16 +34,16 @@ public function index(Request $request) { $daysSinceLastEdit = null; if ($wikiLastEditedTime !== null) { - // Cast to int to avoid fractional-day values in CSV/JSON and keep cutoff logic predictable. + // cast to int to retain Carbon 2 behaviour of using whole days $daysSinceLastEdit = (int) $wikiLastEditedTime->diffInDays($current_date, false); } if ($daysSinceLastEdit !== null && $daysSinceLastEdit >= 90) { - // Keep exported duration as whole days for stable reporting. + // cast to int to retain Carbon 2 behaviour of using whole days $time_before_wiki_abandoned_days = (int) $wiki->created_at->diffInDays($wikiLastEditedTime, false); } if ($wikiFirstEditedTime !== null) { - // Keep exported duration as whole days for stable reporting. + // cast to int to retain Carbon 2 behaviour of using whole days $time_to_engage_days = (int) $wiki->created_at->diffInDays($wikiFirstEditedTime, false); } $wiki_number_of_editors = $wiki->wikiSiteStats()->first()['activeusers'] ?? null; diff --git a/app/Jobs/PlatformStatsSummaryJob.php b/app/Jobs/PlatformStatsSummaryJob.php index 340422abd..a8c5beeb2 100644 --- a/app/Jobs/PlatformStatsSummaryJob.php +++ b/app/Jobs/PlatformStatsSummaryJob.php @@ -132,7 +132,8 @@ public function prepareStats(array $allStats, $wikis): array { // is it edited in the last 90 days? if (!is_null($stats['lastEdit'])) { $lastTimestamp = MWTimestampHelper::getCarbonFromMWTimestamp(intval($stats['lastEdit'])); - // Carbon can return fractional seconds; cast to int so threshold checks use whole-second precision. + // cast to int retain Carbon 2 behaviour of using whole days + // pass `$absolute = true` argument to retain Carbon 2 behaviour of returning an absolute diff $diff = (int) $lastTimestamp->diffInSeconds($currentTime, true); if ($diff <= $this->inactiveThreshold) { diff --git a/app/Jobs/SendEmptyWikiNotificationsJob.php b/app/Jobs/SendEmptyWikiNotificationsJob.php index d80e0e07d..4e56eee75 100644 --- a/app/Jobs/SendEmptyWikiNotificationsJob.php +++ b/app/Jobs/SendEmptyWikiNotificationsJob.php @@ -34,7 +34,7 @@ public function checkIfWikiIsOldAndEmpty(Wiki $wiki) { $emptyDaysThreshold = config('wbstack.wiki_empty_notification_threshold'); $createdAt = $wiki->created_at; $now = CarbonImmutable::now(); - // Normalize to whole days so comparisons with the integer config threshold stay deterministic. + // cast to int to retain Carbon 2 behaviour of using whole days $emptyWikiDays = (int) $createdAt->diffInDays($now, true); $firstEdited = $wiki->wikiLifecycleEvents->first_edited; From 6657a58eae72cb9b5bc218accee7f809d2b6c696 Mon Sep 17 00:00:00 2001 From: Dat WMDE Date: Thu, 28 May 2026 16:07:21 +0200 Subject: [PATCH 14/17] Update test function's name Co-authored-by: Ollie <43674967+outdooracorn@users.noreply.github.com> --- tests/Jobs/SendEmptyWikiNotificationsJobTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php index dd07b544c..cc856097a 100644 --- a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php +++ b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php @@ -51,7 +51,7 @@ public function testEmptyWikiNotificationsSendNotification() { } // empty wikis, that are almost old enough (29 days and 23 hrs) - public function testEmptyWikiNotificationsNotSendNotification() { + public function testEmptyWikiNotificationsNotificationNotSent() { $thresholdDaysAgo = Carbon::now() ->subDays((config('wbstack.wiki_empty_notification_threshold') - 1)) ->subHours(23) From 52a2c5ba104c1929017edc2d0d0cfb3c5d71e151 Mon Sep 17 00:00:00 2001 From: Dat WMDE Date: Thu, 28 May 2026 16:07:46 +0200 Subject: [PATCH 15/17] Apply suggestions from code review Co-authored-by: Ollie <43674967+outdooracorn@users.noreply.github.com> --- tests/Jobs/SendEmptyWikiNotificationsJobTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php index cc856097a..7a2ddf212 100644 --- a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php +++ b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php @@ -64,8 +64,9 @@ public function testEmptyWikiNotificationsNotificationNotSent() { $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); $job = new SendEmptyWikiNotificationsJob; - $this->assertFalse($job->checkIfWikiIsOldAndEmpty($wiki)); $job->handle(); + + Notification::assertNothingSent(); } // fresh wiki that does not have lifecycle event records yet From 46a715f1127eb03bb8ac2e61e2c7976d9a67a4b2 Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 28 May 2026 17:03:02 +0200 Subject: [PATCH 16/17] reverse unnecessary changes --- tests/Routes/Wiki/ConversionMetricTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index 07f031821..24e06ce31 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -58,10 +58,10 @@ private function createTestWiki($name, $createdWeeksAgo, $firstEditedWeeksAgo, $ $wiki->created_at = $current_date->subWeeks($createdWeeksAgo); $events = $wiki->wikiLifecycleEvents(); $update = []; - if ($lastEditedWeeksAgo !== null) { + if ($lastEditedWeeksAgo) { $update['last_edited'] = $current_date->subWeeks($lastEditedWeeksAgo); } - if ($firstEditedWeeksAgo !== null) { + if ($firstEditedWeeksAgo) { $update['first_edited'] = $current_date->subWeeks($firstEditedWeeksAgo); } $events->updateOrCreate($update); From ed03b361943393da3aec1be0a0a0f21eff4db002 Mon Sep 17 00:00:00 2001 From: Dat WMDE Date: Fri, 29 May 2026 16:15:12 +0200 Subject: [PATCH 17/17] Add comment explaining the hack to override the mwHostResolver property Co-authored-by: Ollie <43674967+outdooracorn@users.noreply.github.com> --- tests/Jobs/PlatformStatsSummaryJobTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Jobs/PlatformStatsSummaryJobTest.php b/tests/Jobs/PlatformStatsSummaryJobTest.php index d94e970dc..6a00c74b0 100644 --- a/tests/Jobs/PlatformStatsSummaryJobTest.php +++ b/tests/Jobs/PlatformStatsSummaryJobTest.php @@ -298,6 +298,11 @@ public function testPrepareStatsTreatsSecondPrecisionTimestampAtThresholdAsActiv ]); $job = new PlatformStatsSummaryJob; + + // This is a hack to override the `private` `PlatformStatsSummaryJob::mwHostResolver` property. + // See https://www.php.net/manual/en/closure.call.php for more details on how this works. + // TODO: figure out how to stub the `DatabaseManager` correctly and/or refactor the Job so that + // we can more easily inject dependencies in the tests. (function ($resolver): void { $this->mwHostResolver = $resolver; })->call($job, $this->mockMwHostResolver);