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.
This commit is contained in:
Víctor Falcón 2026-07-15 08:08:03 +02:00
parent c00069f208
commit f7a83788ea
2 changed files with 19 additions and 15 deletions

View File

@ -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'])

View File

@ -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 () {