festbench-web/tests/Feature/SessionSummaryTest.php
PeterChrz 700bdae959
feat: replace search/select filters with 5 multi-select checkbox dropdowns
Remove the free-text search input (exact-string LIKE was misleading) and
the single-value Model/Suite selects. Replace with five checkbox
multi-select dropdowns in display order: GPU, Model, Quant, Inference
engine, Suite.

Matching semantics:
  - OR within each filter control (selecting two GPUs shows either)
  - AND across controls (GPU + Model + Quant + Engine + Suite)
  - Suite selection is inclusive: a band running any selected suite
    matches, including incomplete bands missing some selected suites

Each dropdown shows a count badge when active and a checkmark per selected
item. Pagination resets on every filter change. The cache key already
includes the filters array so filtered results are cached independently.
2026-10-05 00:37:33 -04:00

280 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\BenchmarkRun;
use App\Models\InfrabenchResult;
use App\Models\ScenarioRun;
use App\Models\SoftbenchResult;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Livewire\Volt\Volt;
test('festbench:summary splits runs by label into separate bands', function () {
$me = User::factory()->create(['name' => 'peterZ']);
$a = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'session A', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'AMD R9700', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $a->id, 'ok' => true, 'user_tps' => 60.0, 'total' => 100, 'level' => 'off']);
SoftbenchResult::factory()->create(['benchmark_run_id' => $a->id, 'ok' => false, 'user_tps' => 20.0, 'total' => 300, 'level' => 'off']);
$b = BenchmarkRun::factory()->create([
'suite' => 'infrabench', 'label' => 'session A', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'AMD R9700', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:05:00',
]);
InfrabenchResult::factory()->create(['benchmark_run_id' => $b->id, 'ok' => true, 'user_tps' => 40.0, 'total' => 200, 'level' => 'low']);
$c = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'session B', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'AMD R9700', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-04 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $c->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 50, 'level' => 'medium']);
Artisan::call('festbench:summary');
// Different labels → separate bands.
$bands = DB::table('session_summaries')->orderBy('sequence')->get();
expect($bands)->toHaveCount(2);
$bandA = $bands->firstWhere('label', 'session A');
expect($bandA->runner_name)->toBe('peterZ')
->and($bandA->gpu)->toBe('AMD R9700')
->and($bandA->gpu_count)->toBe(1)
->and($bandA->label)->toBe('session A');
$rowsA = DB::table('session_suite_rows')->where('session_summary_id', $bandA->id)->orderBy('sequence')->get();
expect($rowsA)->toHaveCount(2);
$soft = $rowsA->firstWhere('suite', 'softbench');
expect($soft->passed)->toBe(1)
->and($soft->failed)->toBe(1)
->and($soft->avg_user_tps)->toBe(40.0)
->and($soft->weighted_user_tps)->toBe(30.0)
->and($soft->total_tokens)->toBe(400)
->and($soft->level)->toBe('off');
$infra = $rowsA->firstWhere('suite', 'infrabench');
expect($infra->passed)->toBe(1)->and($infra->level)->toBe('low');
});
test('devlogic and sysops totals are split by suite category', function () {
$me = User::factory()->create();
$soft = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'dual', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => false, 'user_tps' => 10.0, 'total' => 200, 'level' => 'off']);
$infra = BenchmarkRun::factory()->create([
'suite' => 'infrabench', 'label' => 'dual', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:05:00',
]);
InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 150, 'level' => 'low']);
$scen = BenchmarkRun::factory()->create([
'suite' => 'scenariobench', 'label' => 'dual', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:10:00',
]);
ScenarioRun::factory()->create(['benchmark_run_id' => $scen->id, 'ok' => true, 'gen_tokens' => 5000, 'wall' => 100.0]);
ScenarioRun::factory()->create(['benchmark_run_id' => $scen->id, 'ok' => false, 'gen_tokens' => 3000, 'wall' => 200.0]);
Artisan::call('festbench:summary');
$band = DB::table('session_summaries')->where('label', 'dual')->first();
// Devlogic: softbench only — 2 tasks: user_tps=50 (100 tokens),
// user_tps=10 (200 tokens).
// Token-weighted: (50×100 + 10×200) / (100+200) = 7000/300 ≈ 23.3
expect($band->devlogic_passed)->toBe(1)
->and($band->devlogic_failed)->toBe(1)
->and($band->devlogic_tokens)->toBe(300)
->and($band->devlogic_tps)->toBe(23.3);
// Sysops: infrabench (1 task: user_tps=30, tokens=150 → rate_sum=4500)
// + scenariobench (gen_tps = 8000/300 = 26.67, tokens=8000 →
// rate_sum = 26.67×8000 ≈ 213333.33).
// Total sysops tokens = 150 + 8000 = 8150.
// Token-weighted: (4500 + 213333.33) / 8150 ≈ 26.7
expect($band->sysops_passed)->toBe(2)
->and($band->sysops_failed)->toBe(1)
->and($band->sysops_tokens)->toBe(8150);
expect($band->sysops_tps)->not->toBeNull();
expect($band->sysops_tps)->toBeGreaterThan(20.0)->toBeLessThan(30.0);
});
test('private runs form their own band and are hidden from guests', function () {
$me = User::factory()->create();
BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'pub', 'model_id' => 'm', 'visibility' => 'public',
'gpu' => 'g', 'gpu_count' => 1, 'user_id' => $me->id,
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => BenchmarkRun::latest('id')->first()->id]);
BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'pub', 'model_id' => 'm', 'visibility' => 'private',
'gpu' => 'g', 'gpu_count' => 1, 'user_id' => $me->id,
]);
Artisan::call('festbench:summary');
expect(DB::table('session_summaries')->count())->toBe(2)
->and(DB::table('session_summaries')->where('visibility', 'private')->count())->toBe(1);
$this->get(route('home'))->assertOk()->assertSee('pub');
$bands = DB::table('session_summaries')->pluck('visibility');
expect($bands)->toContain('public')->toContain('private');
});
test('home renders the session summary and links into per-task detail', function () {
$run = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'rendered session', 'visibility' => 'public',
'model_id' => 'render/model', 'gpu' => 'AMD R9700', 'gpu_count' => 2,
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id, 'ok' => true, 'level' => 'off']);
Artisan::call('festbench:summary');
$this->get(route('home'))->assertOk()
->assertSee('rendered session')
->assertSee('render/model')
->assertSee('AMD R9700')
->assertSee(route('runs.show', $run->run_id))
->assertSee('All task rows');
});
test('results page still lists flat task rows', function () {
$run = BenchmarkRun::factory()->create(['suite' => 'softbench', 'visibility' => 'public']);
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'flat-task']);
$this->get(route('results'))->assertOk()
->assertSee('All Task Rows')
->assertSee('flat-task');
});
test('multi-filter: suite filter returns bands containing any selected suite', function () {
$me = User::factory()->create();
$soft = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'soft-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
$infra = BenchmarkRun::factory()->create([
'suite' => 'infrabench', 'label' => 'infra-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:05:00',
]);
InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']);
Artisan::call('festbench:summary');
// No filter: both bands visible.
Volt::test('summary.index')->assertSee('soft-only')->assertSee('infra-only');
// Filter to infrabench: only the infra band should match.
Volt::test('summary.index', ['filters' => [
'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['infrabench'],
]])
->assertDontSee('soft-only')
->assertSee('infra-only');
});
test('multi-filter: AND across categories, OR within a category', function () {
$me = User::factory()->create();
// Band A: GPU1 + model-A + Q5_K_M + llama.cpp
$a = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'band-a', 'model_id' => 'model-A', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $a->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
// Band B: GPU2 + model-B + UD-Q4_K_M + ollama-cloud
$b = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'band-b', 'model_id' => 'model-B', 'model_quant' => 'UD-Q4_K_M',
'gpu' => 'GPU2', 'gpu_count' => 1, 'engine' => 'ollama-cloud', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-04 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $b->id, 'ok' => true, 'user_tps' => 40.0, 'total' => 100, 'level' => 'off']);
Artisan::call('festbench:summary');
// GPU filter alone: OR within GPU → both bands.
Volt::test('summary.index', ['filters' => [
'gpu' => ['GPU1', 'GPU2'], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => [],
]])
->assertSee('band-a')
->assertSee('band-b');
// GPU + model: AND across → only matching band.
Volt::test('summary.index', ['filters' => [
'gpu' => ['GPU1', 'GPU2'], 'model' => ['model-A'], 'quant' => [], 'engine' => [], 'suite' => [],
]])
->assertSee('band-a')
->assertDontSee('band-b');
// Engine filter: only llama.cpp band.
Volt::test('summary.index', ['filters' => [
'gpu' => [], 'model' => [], 'quant' => [], 'engine' => ['llama.cpp'], 'suite' => [],
]])
->assertSee('band-a')
->assertDontSee('band-b');
// Quant filter: only UD-Q4_K_M band.
Volt::test('summary.index', ['filters' => [
'gpu' => [], 'model' => [], 'quant' => ['UD-Q4_K_M'], 'engine' => [], 'suite' => [],
]])
->assertDontSee('band-a')
->assertSee('band-b');
});
test('multi-filter: multiple suites use OR for band inclusion', function () {
$me = User::factory()->create();
// Band with only softbench.
$soft = BenchmarkRun::factory()->create([
'suite' => 'softbench', 'label' => 'has-soft', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:00:00',
]);
SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
// Band with only infrabench.
$infra = BenchmarkRun::factory()->create([
'suite' => 'infrabench', 'label' => 'has-infra', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
'ran_at' => '2026-09-03 12:05:00',
]);
InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']);
Artisan::call('festbench:summary');
// Both suites selected → both bands visible (OR within suite).
Volt::test('summary.index', ['filters' => [
'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['softbench', 'infrabench'],
]])
->assertSee('has-soft')
->assertSee('has-infra');
});
test('toggleFilter adds and removes a value from a filter array', function () {
$component = Volt::test('summary.index');
$component->call('toggleFilter', 'suite', 'infrabench')
->assertSet('filters.suite', ['infrabench']);
$component->call('toggleFilter', 'suite', 'infrabench')
->assertSet('filters.suite', []);
});