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); });