164 lines
6.2 KiB
PHP
164 lines
6.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use App\Models\BenchmarkRun;
|
||
|
|
use App\Models\DepthsweepResult;
|
||
|
|
use App\Models\DeviceCode;
|
||
|
|
use App\Models\Gpu;
|
||
|
|
use App\Models\SoftbenchResult;
|
||
|
|
use App\Models\User;
|
||
|
|
use Illuminate\Support\Facades\Artisan;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Livewire\Volt\Volt;
|
||
|
|
|
||
|
|
function start_device_flow(): array
|
||
|
|
{
|
||
|
|
$response = test()->postJson('/api/cli/device');
|
||
|
|
|
||
|
|
$response->assertStatus(201)
|
||
|
|
->assertJsonStructure(['code', 'poll_token', 'verify_url', 'expires_in']);
|
||
|
|
|
||
|
|
return $response->json();
|
||
|
|
}
|
||
|
|
|
||
|
|
test('the device flow grants a one-time usable token', function () {
|
||
|
|
$me = User::factory()->create();
|
||
|
|
$this->actingAs($me);
|
||
|
|
|
||
|
|
$start = start_device_flow();
|
||
|
|
|
||
|
|
// Poll before confirming: pending
|
||
|
|
$this->postJson('/api/cli/poll', ['code' => $start['code'], 'poll_token' => $start['poll_token']])
|
||
|
|
->assertOk()->assertJson(['status' => 'pending']);
|
||
|
|
|
||
|
|
// Confirm via the authorize screen logic (the grant path the page runs)
|
||
|
|
$device = DeviceCode::where('code', $start['code'])->first();
|
||
|
|
$plain = $me->createToken('festbench-cli', ['gpus:read', 'results:submit'], now()->addYear());
|
||
|
|
$device->forceFill([
|
||
|
|
'user_id' => $me->id,
|
||
|
|
'personal_access_token_id' => $plain->accessToken->id,
|
||
|
|
'plain_token' => $plain->plainTextToken,
|
||
|
|
])->save();
|
||
|
|
|
||
|
|
// Poll after: granted with a usable id|secret token
|
||
|
|
$granted = $this->postJson('/api/cli/poll', ['code' => $start['code'], 'poll_token' => $start['poll_token']])
|
||
|
|
->assertOk()->assertJson(['status' => 'granted'])
|
||
|
|
->json();
|
||
|
|
|
||
|
|
expect($granted['token'])->toContain('|')
|
||
|
|
->and($granted['abilities'])->toContain('gpus:read');
|
||
|
|
|
||
|
|
// The token authenticates against the API
|
||
|
|
$this->withToken($granted['token'])->getJson('/api/me')
|
||
|
|
->assertOk()->assertJson(['id' => $me->id, 'email' => $me->email]);
|
||
|
|
|
||
|
|
$this->withToken($granted['token'])->getJson('/api/gpus')->assertOk();
|
||
|
|
|
||
|
|
// Replay: bare granted, no second token handout
|
||
|
|
$replay = $this->postJson('/api/cli/poll', ['code' => $start['code'], 'poll_token' => $start['poll_token']])
|
||
|
|
->assertOk()->assertJson(['status' => 'granted'])->json();
|
||
|
|
|
||
|
|
expect($replay)->not->toHaveKey('token');
|
||
|
|
|
||
|
|
// The secret is erased server-side after pickup
|
||
|
|
expect(DeviceCode::where('code', $start['code'])->first()->plain_token)->toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('poll with a wrong token is rejected without an oracle', function () {
|
||
|
|
$start = start_device_flow();
|
||
|
|
|
||
|
|
$this->postJson('/api/cli/poll', ['code' => $start['code'], 'poll_token' => 'wrong-token-entirely'])
|
||
|
|
->assertStatus(404);
|
||
|
|
|
||
|
|
$this->postJson('/api/cli/poll', ['code' => 'NOPE-NOPE', 'poll_token' => $start['poll_token']])
|
||
|
|
->assertStatus(404);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('expired codes report expired on poll', function () {
|
||
|
|
$start = start_device_flow();
|
||
|
|
|
||
|
|
DeviceCode::where('code', $start['code'])->update(['expires_at' => now()->subMinute()]);
|
||
|
|
|
||
|
|
$this->postJson('/api/cli/poll', ['code' => $start['code'], 'poll_token' => $start['poll_token']])
|
||
|
|
->assertOk()->assertJson(['status' => 'expired']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the gpu catalog requires authentication and the gpus:read ability', function () {
|
||
|
|
// Guest: redirected to login (no token at all)
|
||
|
|
$this->getJson('/api/gpus')->assertStatus(401);
|
||
|
|
|
||
|
|
// Token with the wrong ability
|
||
|
|
$me = User::factory()->create();
|
||
|
|
$limited = $me->createToken('no-gpus', ['results:submit'], now()->addYear());
|
||
|
|
|
||
|
|
$this->withToken($limited->plainTextToken)->getJson('/api/gpus')->assertStatus(403);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the catalog returns only active gpus', function () {
|
||
|
|
$me = User::factory()->create();
|
||
|
|
$token = $me->createToken('reader', ['gpus:read'], now()->addYear());
|
||
|
|
|
||
|
|
// Seed the catalog for this test (RefreshDatabase resets between tests)
|
||
|
|
Artisan::call('db:seed', ['--class' => 'GpuSeeder', '--force' => true]);
|
||
|
|
|
||
|
|
// Deactivate one entry
|
||
|
|
DB::table('gpus')->where('name', 'AMD Radeon RX 7900 XTX')->update(['active' => false]);
|
||
|
|
|
||
|
|
$response = $this->withToken($token->plainTextToken)->getJson('/api/gpus')->assertOk()->json();
|
||
|
|
|
||
|
|
$names = array_column($response['gpus'], 'name');
|
||
|
|
expect($names)->toContain('AMD Radeon AI PRO R9700')
|
||
|
|
->not->toContain('AMD Radeon RX 7900 XTX');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('gpu confirm on my-runs links the run and regroups it', function () {
|
||
|
|
Artisan::call('db:seed', ['--class' => 'GpuSeeder', '--force' => true]);
|
||
|
|
|
||
|
|
$me = User::factory()->create();
|
||
|
|
|
||
|
|
// A run whose raw gpu string missed the catalog (typo case)
|
||
|
|
$run = BenchmarkRun::factory()->create([
|
||
|
|
'suite' => 'softbench', 'user_id' => $me->id, 'gpu' => 'amd radeon ai pro r9700', 'gpu_id' => null,
|
||
|
|
]);
|
||
|
|
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id]);
|
||
|
|
Artisan::call('festbench:summary');
|
||
|
|
|
||
|
|
// Owner confirms via the picker flow
|
||
|
|
$this->actingAs($me);
|
||
|
|
Volt::test('my-runs.index')
|
||
|
|
->call('startGpuConfirm', $run->id)
|
||
|
|
->call('confirmGpu', Gpu::match('AMD Radeon AI PRO R9700')->id);
|
||
|
|
|
||
|
|
$run->refresh();
|
||
|
|
expect($run->gpu_id)->not->toBeNull()
|
||
|
|
->and($run->gpu)->toBe('AMD Radeon AI PRO R9700');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('depthsweep results import and aggregate without tok/s', function () {
|
||
|
|
$run = BenchmarkRun::factory()->create(['suite' => 'depthsweep', 'visibility' => 'public']);
|
||
|
|
DepthsweepResult::factory()->count(5)->create(['benchmark_run_id' => $run->id]);
|
||
|
|
|
||
|
|
Artisan::call('festbench:summary');
|
||
|
|
|
||
|
|
$row = DB::table('session_suite_rows')->where('suite', 'depthsweep')->first();
|
||
|
|
expect($row->passed)->toBeInt()
|
||
|
|
->and($row->avg_user_tps)->toBeNull()
|
||
|
|
->and($row->total_tokens)->toBe(0);
|
||
|
|
|
||
|
|
// Run detail renders the depthsweep column set
|
||
|
|
$this->get(route('runs.show', $run->run_id))->assertOk()
|
||
|
|
->assertSee('Depth sweep results')
|
||
|
|
->assertSee('Needle');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('session bands show column titles', function () {
|
||
|
|
$run = BenchmarkRun::factory()->create(['suite' => 'softbench', 'visibility' => 'public']);
|
||
|
|
SoftbenchResult::factory()->create(['benchmark_run_id' => $run->id]);
|
||
|
|
Artisan::call('festbench:summary');
|
||
|
|
|
||
|
|
$this->get(route('home'))->assertOk()
|
||
|
|
->assertSee('Benchmark', false)
|
||
|
|
->assertSee('Thinking', false)
|
||
|
|
->assertSee('Speed (tok/s)', false)
|
||
|
|
->assertSee('Tokens', false);
|
||
|
|
});
|