'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); } public function test_sample_metrics_persists_a_row_from_the_cached_reading(): void { $s = $this->server(); Cache::put("metrics:latest:{$s->id}", ['cpu' => 40, 'mem' => 55, 'disk' => 12, 'load' => 0.7], now()->addMinutes(5)); $this->artisan('clusev:sample-metrics')->assertSuccessful(); $this->assertDatabaseHas('metric_samples', ['server_id' => $s->id, 'cpu' => 40, 'mem' => 55, 'disk' => 12]); $this->assertSame(1, MetricSample::count()); } public function test_sample_metrics_skips_a_server_with_no_cached_reading(): void { $this->server(); // no cached reading → no sample (honest gap) $this->artisan('clusev:sample-metrics')->assertSuccessful(); $this->assertSame(0, MetricSample::count()); } public function test_sample_metrics_prunes_beyond_the_retention_window(): void { $s = $this->server(); MetricSample::create(['server_id' => $s->id, 'cpu' => 1, 'mem' => 1, 'disk' => 1, 'load' => 0, 'sampled_at' => now()->subDays(40)]); MetricSample::create(['server_id' => $s->id, 'cpu' => 2, 'mem' => 2, 'disk' => 2, 'load' => 0, 'sampled_at' => now()->subDays(5)]); $this->artisan('clusev:sample-metrics', ['--retention' => 30])->assertSuccessful(); $this->assertDatabaseMissing('metric_samples', ['cpu' => 1]); // 40d old → pruned $this->assertDatabaseHas('metric_samples', ['cpu' => 2]); // 5d old → kept } public function test_series_buckets_and_averages_samples_with_null_gaps(): void { $s = $this->server(); MetricSample::create(['server_id' => $s->id, 'cpu' => 20, 'mem' => 40, 'disk' => 10, 'load' => 0.5, 'sampled_at' => now()->subSeconds(90)]); MetricSample::create(['server_id' => $s->id, 'cpu' => 40, 'mem' => 60, 'disk' => 10, 'load' => 1.5, 'sampled_at' => now()->subSeconds(80)]); $series = app(MetricHistory::class)->series($s, 300, 10); $this->assertSame(300, $series['window']); $this->assertCount(10, $series['points']); $cpu = array_column($series['points'], 'cpu'); // the bucket holding both samples averages them (20 + 40) / 2 = 30 $this->assertContains(30, $cpu); // empty buckets stay null — gaps, not fabricated values $this->assertContains(null, $cpu); } public function test_show_passes_a_history_series_to_the_view(): void { $this->actingAs(User::factory()->create()); $s = $this->server(); MetricSample::create(['server_id' => $s->id, 'cpu' => 30, 'mem' => 40, 'disk' => 10, 'load' => 0.3, 'sampled_at' => now()->subMinutes(5)]); Livewire::test(Show::class, ['server' => $s]) ->assertViewHas('history', fn ($h) => is_array($h) && isset($h['points']) && $h['window'] === 86400); } public function test_set_range_clamps_to_allowed_keys(): void { $this->actingAs(User::factory()->create()); $s = $this->server(); Livewire::test(Show::class, ['server' => $s]) ->assertSet('historyRange', '24h') ->call('setRange', '7d')->assertSet('historyRange', '7d') ->call('setRange', 'bogus')->assertSet('historyRange', '7d'); // invalid → unchanged } public function test_a_direct_invalid_range_property_is_clamped(): void { $this->actingAs(User::factory()->create()); $s = $this->server(); // historyRange is a public prop — a client can set it directly, bypassing setRange(). Livewire::test(Show::class, ['server' => $s]) ->set('historyRange', 'evil') ->assertSet('historyRange', '24h'); } }