Phase 2: benchmark_runs + run_results schema, models with visibility scope and immutability guard, factories, tests
This commit is contained in:
parent
6d5f9d1ac3
commit
49033c2456
19 changed files with 1593 additions and 342 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
|||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
|
||||
/** @var MustVerifyEmail $user */
|
||||
$user = $request->user();
|
||||
|
||||
event(new Verified($user));
|
||||
|
|
|
|||
97
app/Models/BenchmarkRun.php
Normal file
97
app/Models/BenchmarkRun.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\BenchmarkRunFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BenchmarkRun extends Model
|
||||
{
|
||||
/** @use HasFactory<BenchmarkRunFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public const VISIBILITY_PUBLIC = 'public';
|
||||
|
||||
public const VISIBILITY_PRIVATE = 'private';
|
||||
|
||||
/**
|
||||
* Columns written only at submission/import time. Everything here is
|
||||
* immutable once stored; user updates may only touch $overlay.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'run_id', 'ran_at', 'suite', 'run_key',
|
||||
'model_id', 'model_quant', 'model_quant_raw', 'model_path',
|
||||
'engine', 'engine_build', 'n_ctx', 'slots',
|
||||
'gpu', 'gpu_count', 'gpu_source', 'host', 'client', 'endpoint',
|
||||
'label', 'config', 'image_digest', 'validators', 'suite_versions',
|
||||
'harness', 'harness_ver', 'harness_mode', 'user_id',
|
||||
'visibility', 'notes',
|
||||
];
|
||||
|
||||
/** The only fields a user may change after submission. */
|
||||
public const OVERLAY_FIELDS = ['visibility', 'notes'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'ran_at' => 'datetime',
|
||||
'validators' => 'array',
|
||||
'suite_versions' => 'array',
|
||||
'n_ctx' => 'integer',
|
||||
'slots' => 'integer',
|
||||
'gpu_count' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Submitted metrics are immutable: any update that touches a column
|
||||
* outside OVERLAY_FIELDS is silently reverted before it hits the
|
||||
* database. This is the safety net for every user-facing update path.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updating(function (self $run): void {
|
||||
foreach ($run->getDirty() as $key => $_) {
|
||||
if (! in_array($key, self::OVERLAY_FIELDS, true)) {
|
||||
$run->setAttribute($key, $run->getOriginal($key));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function results(): HasMany
|
||||
{
|
||||
return $this->hasMany(RunResult::class);
|
||||
}
|
||||
|
||||
public function isOwnedBy(?User $user): bool
|
||||
{
|
||||
return $user !== null && $this->user_id === $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs visible to $user: everything public, plus the user's own private
|
||||
* runs. Applied as a local scope so every listing query starts from it.
|
||||
*/
|
||||
public function scopeVisibleTo(Builder $query, ?User $user): Builder
|
||||
{
|
||||
return $query->where(function (Builder $q) use ($user) {
|
||||
$q->where('visibility', self::VISIBILITY_PUBLIC);
|
||||
|
||||
if ($user !== null) {
|
||||
$q->orWhere(fn (Builder $own) => $own
|
||||
->where('visibility', self::VISIBILITY_PRIVATE)
|
||||
->where('user_id', $user->id));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
45
app/Models/RunResult.php
Normal file
45
app/Models/RunResult.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\RunResultFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RunResult extends Model
|
||||
{
|
||||
/** @use HasFactory<RunResultFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'benchmark_run_id', 'suite', 'section', 'task', 'level',
|
||||
'ok', 'decode', 'user_tps', 'ttft', 't_answer', 'wall',
|
||||
'reasoning', 'visible', 'total', 'tool_calls',
|
||||
'checks_passed', 'checks_total', 'finish', 'detail', 'payload',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'ok' => 'boolean',
|
||||
'decode' => 'float',
|
||||
'user_tps' => 'float',
|
||||
'ttft' => 'float',
|
||||
't_answer' => 'float',
|
||||
'wall' => 'float',
|
||||
'reasoning' => 'integer',
|
||||
'visible' => 'integer',
|
||||
'total' => 'integer',
|
||||
'tool_calls' => 'integer',
|
||||
'checks_passed' => 'integer',
|
||||
'checks_total' => 'integer',
|
||||
'payload' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function run(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BenchmarkRun::class, 'benchmark_run_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
|
||||
class User extends Authenticatable // implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
|
@ -20,9 +22,9 @@ public function boot(): void
|
|||
Fortify::ignoreRoutes();
|
||||
|
||||
Fortify::authenticateUsing(function (Request $request) {
|
||||
$user = \App\Models\User::where('email', $request->email)->first();
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if ($user && \Illuminate\Support\Facades\Hash::check($request->password, $user->password)) {
|
||||
if ($user && Hash::check($request->password, $user->password)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\FortifyServiceProvider;
|
||||
use App\Providers\VoltServiceProvider;
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
App\Providers\VoltServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
FortifyServiceProvider::class,
|
||||
VoltServiceProvider::class,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
"pestphp/pest": "^4.1",
|
||||
"pestphp/pest-plugin-laravel": "^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
1458
composer.lock
generated
1458
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
|
@ -62,7 +64,7 @@
|
|||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
'options' => extension_loaded('pdo_mysql') && env('MYSQL_ATTR_SSL_CA') ? [
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
] : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
|
|
@ -77,9 +77,9 @@
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
'options' => extension_loaded('pdo_mysql') && env('MYSQL_ATTR_SSL_CA') ? [
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
] : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
|
|
|
|||
54
database/factories/BenchmarkRunFactory.php
Normal file
54
database/factories/BenchmarkRunFactory.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\BenchmarkRun;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends Factory<BenchmarkRun> */
|
||||
class BenchmarkRunFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$quant = $this->faker->randomElement(['Q4_K_M', 'Q5_K_M', 'Q6_K', 'Q8_0', 'fp8', 'none']);
|
||||
|
||||
return [
|
||||
'run_id' => str()->random(12),
|
||||
'ran_at' => $this->faker->dateTimeThisMonth(),
|
||||
'suite' => $this->faker->randomElement(['softbench', 'infrabench', 'prosebench', 'psychbench']),
|
||||
'run_key' => null,
|
||||
'model_id' => $this->faker->randomElement([
|
||||
'Qwen/Qwen3-8B', 'bartowski/Qwen3.8-27B-GGUF', 'meta-llama/Llama-3.3-70B',
|
||||
]),
|
||||
'model_quant' => $quant === 'none' ? null : $quant,
|
||||
'model_quant_raw' => $quant === 'none' ? null : $quant,
|
||||
'model_path' => null,
|
||||
'engine' => $this->faker->randomElement(['llama.cpp', 'vllm', 'unknown']),
|
||||
'engine_build' => 'b10711-9723942ad',
|
||||
'n_ctx' => $this->faker->randomElement([32768, 65536, 131072]),
|
||||
'slots' => 1,
|
||||
'gpu' => $this->faker->randomElement(['unknown', 'RTX 4090', 'RX 7900 XTX']),
|
||||
'gpu_count' => $this->faker->numberBetween(0, 2),
|
||||
'gpu_source' => 'remote-endpoint',
|
||||
'host' => null,
|
||||
'client' => null,
|
||||
'endpoint' => null,
|
||||
'label' => $this->faker->optional()->word(),
|
||||
'config' => null,
|
||||
'image_digest' => 'quay.io/pch/festbench@sha256:'.str()->random(64),
|
||||
'validators' => null,
|
||||
'suite_versions' => null,
|
||||
'harness' => 'direct',
|
||||
'harness_ver' => null,
|
||||
'harness_mode' => null,
|
||||
'user_id' => null,
|
||||
'visibility' => BenchmarkRun::VISIBILITY_PUBLIC,
|
||||
'notes' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function private(): static
|
||||
{
|
||||
return $this->state(fn () => ['visibility' => BenchmarkRun::VISIBILITY_PRIVATE]);
|
||||
}
|
||||
}
|
||||
54
database/factories/RunResultFactory.php
Normal file
54
database/factories/RunResultFactory.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\BenchmarkRun;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends Factory<RunResult> */
|
||||
class RunResultFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$visible = $this->faker->numberBetween(20, 400);
|
||||
$reasoning = $this->faker->numberBetween(0, 2000);
|
||||
$ttft = round($this->faker->randomFloat(2, 0.1, 4), 2);
|
||||
$wall = round($ttft + $this->faker->randomFloat(2, 0.5, 60), 2);
|
||||
$ok = $this->faker->boolean(70);
|
||||
|
||||
$payload = [
|
||||
'level' => null, 'section' => null, 'name' => null, 'ok' => $ok,
|
||||
'ttft' => $ttft, 'wall' => $wall,
|
||||
];
|
||||
|
||||
return [
|
||||
'benchmark_run_id' => BenchmarkRun::factory(),
|
||||
'suite' => 'softbench',
|
||||
'section' => $this->faker->randomElement(['code', 'reasoning', 'constraints', null]),
|
||||
'task' => $this->faker->randomElement([
|
||||
'first_occurrence', 'merge_intervals', 'version_compare', 'rbac_missing_verb', 'oom_innocent_victim',
|
||||
]),
|
||||
'level' => $this->faker->randomElement(['off', 'low', 'medium', 'xhigh']),
|
||||
'ok' => $ok,
|
||||
'decode' => round($this->faker->randomFloat(2, 10, 150), 2),
|
||||
'user_tps' => round($this->faker->randomFloat(2, 5, 100), 2),
|
||||
'ttft' => $ttft,
|
||||
't_answer' => $ttft,
|
||||
'wall' => $wall,
|
||||
'reasoning' => $reasoning,
|
||||
'visible' => $visible,
|
||||
'total' => $visible + $reasoning,
|
||||
'tool_calls' => null,
|
||||
'checks_passed' => null,
|
||||
'checks_total' => null,
|
||||
'finish' => $ok ? 'stop' : $this->faker->randomElement(['stop', 'length']),
|
||||
'detail' => null,
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
public function forSuite(string $suite): static
|
||||
{
|
||||
return $this->state(fn () => ['suite' => $suite]);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('benchmark_runs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('run_id', 32)->unique()->comment('festbench run_id');
|
||||
$table->dateTime('ran_at')->nullable()->comment('ts from festbench');
|
||||
$table->string('suite', 32);
|
||||
$table->string('run_key', 255)->nullable();
|
||||
|
||||
// model + server identity (sensitive fields nullable, redacted by default)
|
||||
$table->string('model_id', 255)->nullable();
|
||||
$table->string('model_quant', 64)->nullable();
|
||||
$table->string('model_quant_raw', 128)->nullable();
|
||||
$table->string('model_path', 512)->nullable();
|
||||
$table->string('engine', 64)->nullable();
|
||||
$table->string('engine_build', 128)->nullable();
|
||||
$table->unsignedBigInteger('n_ctx')->nullable();
|
||||
$table->unsignedInteger('slots')->nullable();
|
||||
$table->string('gpu', 128)->nullable();
|
||||
$table->unsignedInteger('gpu_count')->nullable();
|
||||
$table->string('gpu_source', 64)->nullable();
|
||||
$table->string('host', 255)->nullable();
|
||||
$table->string('client', 255)->nullable();
|
||||
$table->string('endpoint', 512)->nullable();
|
||||
$table->string('label', 255)->nullable();
|
||||
$table->string('config', 255)->nullable();
|
||||
|
||||
// provenance
|
||||
$table->string('image_digest', 512)->nullable();
|
||||
$table->json('validators')->nullable();
|
||||
$table->json('suite_versions')->nullable();
|
||||
$table->string('harness', 32)->nullable();
|
||||
$table->string('harness_ver', 64)->nullable();
|
||||
$table->string('harness_mode', 64)->nullable();
|
||||
|
||||
// user-owned overlay — the only mutable part
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('visibility', 16)->default('public')->comment('public|private');
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('suite');
|
||||
$table->index(['model_id', 'model_quant']);
|
||||
$table->index(['visibility', 'user_id']);
|
||||
});
|
||||
|
||||
Schema::create('run_results', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('benchmark_run_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('suite', 32);
|
||||
$table->string('section', 64)->nullable();
|
||||
$table->string('task', 128);
|
||||
$table->string('level', 16)->nullable()->comment('off|low|medium|xhigh');
|
||||
|
||||
$table->boolean('ok')->nullable();
|
||||
$table->double('decode')->nullable();
|
||||
$table->double('user_tps')->nullable();
|
||||
$table->double('ttft')->nullable();
|
||||
$table->double('t_answer')->nullable();
|
||||
$table->double('wall')->nullable();
|
||||
$table->unsignedInteger('reasoning')->nullable();
|
||||
$table->unsignedInteger('visible')->nullable();
|
||||
$table->unsignedInteger('total')->nullable();
|
||||
$table->unsignedInteger('tool_calls')->nullable();
|
||||
$table->unsignedInteger('checks_passed')->nullable();
|
||||
$table->unsignedInteger('checks_total')->nullable();
|
||||
$table->string('finish', 32)->nullable();
|
||||
$table->text('detail')->nullable();
|
||||
|
||||
// complete source row, so future festbench fields need no migration
|
||||
$table->json('payload')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['benchmark_run_id']);
|
||||
$table->index(['suite', 'level']);
|
||||
$table->index(['ok']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('run_results');
|
||||
Schema::dropIfExists('benchmark_runs');
|
||||
}
|
||||
};
|
||||
7
resources/views/layouts/app.blade.php
Normal file
7
resources/views/layouts/app.blade.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<x-layouts.app.header :title="$title ?? null">
|
||||
<flux:main>
|
||||
<div class="mx-auto w-full max-w-7xl">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</flux:main>
|
||||
</x-layouts.app.header>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||
use App\Livewire\Actions\Logout;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
|
|
@ -31,5 +32,5 @@
|
|||
->name('password.confirm');
|
||||
});
|
||||
|
||||
Route::post('logout', App\Livewire\Actions\Logout::class)
|
||||
Route::post('logout', Logout::class)
|
||||
->name('logout');
|
||||
|
|
|
|||
3
t.php
Normal file
3
t.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
echo 'ok';
|
||||
72
tests/Feature/BenchmarkRunTest.php
Normal file
72
tests/Feature/BenchmarkRunTest.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
use App\Models\BenchmarkRun;
|
||||
use App\Models\RunResult;
|
||||
use App\Models\User;
|
||||
|
||||
test('guests see only public runs', function () {
|
||||
$publicRun = BenchmarkRun::factory()->create(['visibility' => 'public']);
|
||||
BenchmarkRun::factory()->create(['visibility' => 'private']);
|
||||
|
||||
$visible = BenchmarkRun::visibleTo(null)->get();
|
||||
|
||||
expect($visible->pluck('id'))->toContain($publicRun->id)
|
||||
->and($visible)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('authenticated users see public runs plus their own private runs', function () {
|
||||
$me = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
|
||||
$publicRun = BenchmarkRun::factory()->create(['visibility' => 'public']);
|
||||
$myPrivate = BenchmarkRun::factory()->create(['visibility' => 'private', 'user_id' => $me->id]);
|
||||
BenchmarkRun::factory()->create(['visibility' => 'private', 'user_id' => $other->id]);
|
||||
|
||||
$visible = BenchmarkRun::visibleTo($me)->get();
|
||||
|
||||
expect($visible->pluck('id'))->toContain($publicRun->id)
|
||||
->and($visible->pluck('id'))->toContain($myPrivate->id)
|
||||
->and($visible)->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('submitted metrics are immutable through fillable updates', function () {
|
||||
$run = BenchmarkRun::factory()->create(['model_id' => 'original/model', 'visibility' => 'public']);
|
||||
|
||||
// The only update path any user-facing code may use:
|
||||
$run->fill(['notes' => 'my notes', 'visibility' => 'private', 'model_id' => 'hacked/model'])
|
||||
->save();
|
||||
|
||||
expect($run->notes)->toBe('my notes')
|
||||
->and($run->visibility)->toBe('private')
|
||||
->and($run->fresh()->model_id)->toBe('original/model')
|
||||
// fill() must have discarded the immutable field, not persisted it
|
||||
->and($run->getOriginal('model_id'))->toBe('original/model');
|
||||
});
|
||||
|
||||
test('overlay helper reports ownership correctly', function () {
|
||||
$owner = User::factory()->create();
|
||||
$run = BenchmarkRun::factory()->create(['user_id' => $owner->id]);
|
||||
|
||||
expect($run->isOwnedBy($owner))->toBeTrue()
|
||||
->and($run->isOwnedBy(User::factory()->create()))->toBeFalse()
|
||||
->and($run->isOwnedBy(null))->toBeFalse();
|
||||
});
|
||||
|
||||
test('run results belong to a run and are deleted with it', function () {
|
||||
$run = BenchmarkRun::factory()->create();
|
||||
$result = RunResult::factory()->create(['benchmark_run_id' => $run->id]);
|
||||
|
||||
expect($result->run->id)->toBe($run->id);
|
||||
|
||||
$run->delete();
|
||||
|
||||
expect(RunResult::where('benchmark_run_id', $run->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('run results cast their payload to array', function () {
|
||||
$result = RunResult::factory()->create(['payload' => ['finish' => 'stop', 'ok' => true]]);
|
||||
|
||||
expect($result->fresh()->payload)->toBeArray()
|
||||
->and($result->fresh()->payload['finish'])->toBe('stop')
|
||||
->and($result->fresh()->ok)->toBeBool();
|
||||
});
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|
|
@ -11,8 +14,8 @@
|
|||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
pest()->extend(TestCase::class)
|
||||
->use(RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
/*
|
||||
|
|
|
|||
Loading…
Reference in a new issue