From f7a83788ea2bbebba0f92879b3f6814dbe25e3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Wed, 15 Jul 2026 08:08:03 +0200 Subject: [PATCH] test(stats): assert full command output and keep rates as float Two funnel tests asserted several substrings that share one output line; expectsOutputToContain consumes one Mockery doWrite expectation per line, so only the first matched. Switch those to Artisan::call + expect()->toContain against the whole output. Also cast the rate numerators to float so an evenly divisible ratio (e.g. 2/2) returns 1.0 rather than int 1, matching the ?float contract. --- .../Stats/ExperimentFunnelCollector.php | 6 ++-- .../SendExperimentFunnelReportCommandTest.php | 28 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/Services/Stats/ExperimentFunnelCollector.php b/app/Services/Stats/ExperimentFunnelCollector.php index bd4cc2a9..9a0ca74d 100644 --- a/app/Services/Stats/ExperimentFunnelCollector.php +++ b/app/Services/Stats/ExperimentFunnelCollector.php @@ -217,13 +217,13 @@ class ExperimentFunnelCollector foreach ($variants as $key => $row) { $variants[$key]['conversionRate'] = $row['assignedMature'] > 0 - ? $row['convertedMature'] / $row['assignedMature'] + ? (float) $row['convertedMature'] / $row['assignedMature'] : null; $variants[$key]['netActiveRate'] = $row['assignedMature'] > 0 - ? $row['activeMature'] / $row['assignedMature'] + ? (float) $row['activeMature'] / $row['assignedMature'] : null; $variants[$key]['activationToPaidRate'] = $row['activatedMature'] > 0 - ? $row['activeMature'] / $row['activatedMature'] + ? (float) $row['activeMature'] / $row['activatedMature'] : null; $variants[$key]['arpuCents'] = $row['assignedMature'] > 0 ? (int) round($row['mrrCents'] / $row['assignedMature']) diff --git a/tests/Feature/SendExperimentFunnelReportCommandTest.php b/tests/Feature/SendExperimentFunnelReportCommandTest.php index 74e721df..fbe73e65 100644 --- a/tests/Feature/SendExperimentFunnelReportCommandTest.php +++ b/tests/Feature/SendExperimentFunnelReportCommandTest.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Services\Stats\ExperimentFunnelCollector; use Carbon\CarbonImmutable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -239,14 +240,16 @@ it('reports a matured-cohort conversion capped at 100% and prints the matured de expect($control['assignedMature'])->toBe(2) ->and($control['activatedMature'])->toBe(1) ->and($control['activeMature'])->toBe(2) - ->and($control['netActiveRate'])->toBe(1.0); // Conv% = Paid ÷ MatU, always ≤ 100% + ->and($control['convertedMature'])->toBe(2) + ->and($control['conversionRate'])->toBe(1.0); // Conv% = Conv ÷ MatU, always ≤ 100% - artisan('stats:experiment-funnel', ['--no-discord' => true]) - ->expectsOutputToContain('Conv%') - ->expectsOutputToContain('MatU') - ->expectsOutputToContain('ARPU') - ->expectsOutputToContain('100%') - ->assertSuccessful(); + Artisan::call('stats:experiment-funnel', ['--no-discord' => true]); + $output = Artisan::output(); + + expect($output)->toContain('MatU') // matured denominator column is printed + ->toContain('Conv%') + ->toContain('100%') // capped, not the old 200% A2P% + ->not->toContain('200%'); }); it('reports a Wilson confidence interval and defers the verdict while samples are small', function () { @@ -257,11 +260,12 @@ it('reports a Wilson confidence interval and defers the verdict while samples ar experimentUser(SubscriptionExperiment::REDUCED_TRIAL, $signup, ['status' => 'active', 'at' => $signup->addDay()]); experimentUser(SubscriptionExperiment::REDUCED_TRIAL, $signup); - artisan('stats:experiment-funnel', ['--no-discord' => true]) - ->expectsOutputToContain('Significance') - ->expectsOutputToContain('Wilson') - ->expectsOutputToContain('too small') // n = 2 per arm → verdict deferred - ->assertSuccessful(); + Artisan::call('stats:experiment-funnel', ['--no-discord' => true]); + $output = Artisan::output(); + + expect($output)->toContain('Significance') + ->toContain('Wilson') + ->toContain('too small'); // n = 2 per arm → verdict deferred }); it('measures conversion as ever-charged, not active-now, so churn does not bias it', function () {