Studio: make grid checkbox rectangular and commit table changes
This commit is contained in:
278
tests/Feature/RisingEngineTest.php
Normal file
278
tests/Feature/RisingEngineTest.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkStats;
|
||||
use App\Models\ArtworkMetricSnapshotHourly;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Helper: create an artwork row without triggering observers (avoids GREATEST() SQLite issue).
|
||||
*/
|
||||
function createArtworkWithoutObserver(array $attrs = []): Artwork
|
||||
{
|
||||
return Artwork::withoutEvents(function () use ($attrs) {
|
||||
return Artwork::factory()->create($attrs);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Snapshot Collection Command ───────────────────────────────────────────
|
||||
|
||||
it('nova:metrics-snapshot-hourly runs without errors', function () {
|
||||
$this->artisan('nova:metrics-snapshot-hourly --dry-run')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('creates snapshot rows for eligible artworks', function () {
|
||||
$artwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
ArtworkStats::upsert([
|
||||
[
|
||||
'artwork_id' => $artwork->id,
|
||||
'views' => 100,
|
||||
'downloads' => 10,
|
||||
'favorites' => 5,
|
||||
'comments_count' => 2,
|
||||
'shares_count' => 1,
|
||||
],
|
||||
], ['artwork_id']);
|
||||
|
||||
$this->artisan('nova:metrics-snapshot-hourly')
|
||||
->assertSuccessful();
|
||||
|
||||
$snapshot = ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->first();
|
||||
expect($snapshot)->not->toBeNull();
|
||||
expect((int) $snapshot->views_count)->toBe(100);
|
||||
expect((int) $snapshot->downloads_count)->toBe(10);
|
||||
expect((int) $snapshot->favourites_count)->toBe(5);
|
||||
});
|
||||
|
||||
it('upserts on duplicate bucket_hour', function () {
|
||||
$artwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
ArtworkStats::upsert([
|
||||
[
|
||||
'artwork_id' => $artwork->id,
|
||||
'views' => 50,
|
||||
'downloads' => 5,
|
||||
'favorites' => 2,
|
||||
],
|
||||
], ['artwork_id']);
|
||||
|
||||
// Run twice — should not throw
|
||||
$this->artisan('nova:metrics-snapshot-hourly')->assertSuccessful();
|
||||
|
||||
// Update stats and run again
|
||||
ArtworkStats::where('artwork_id', $artwork->id)->update(['views' => 75]);
|
||||
$this->artisan('nova:metrics-snapshot-hourly')->assertSuccessful();
|
||||
|
||||
$count = ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count();
|
||||
expect($count)->toBe(1); // upserted, not duplicated
|
||||
|
||||
$snapshot = ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->first();
|
||||
expect((int) $snapshot->views_count)->toBe(75);
|
||||
});
|
||||
|
||||
// ─── Heat Recalculation Command ────────────────────────────────────────────
|
||||
|
||||
it('nova:recalculate-heat runs without errors', function () {
|
||||
$this->artisan('nova:recalculate-heat --dry-run')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('computes heat_score from snapshot deltas', function () {
|
||||
$artwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subHours(2),
|
||||
]);
|
||||
|
||||
ArtworkStats::upsert([
|
||||
['artwork_id' => $artwork->id, 'views' => 0, 'downloads' => 0, 'favorites' => 0],
|
||||
], ['artwork_id']);
|
||||
|
||||
$prevHour = now()->startOfHour()->subHour();
|
||||
$currentHour = now()->startOfHour();
|
||||
|
||||
// Previous hour snapshot
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => $prevHour,
|
||||
'views_count' => 10,
|
||||
'downloads_count' => 2,
|
||||
'favourites_count' => 1,
|
||||
'comments_count' => 0,
|
||||
'shares_count' => 0,
|
||||
]);
|
||||
|
||||
// Current hour snapshot (engagement grew)
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => $currentHour,
|
||||
'views_count' => 30,
|
||||
'downloads_count' => 5,
|
||||
'favourites_count' => 4,
|
||||
'comments_count' => 2,
|
||||
'shares_count' => 1,
|
||||
]);
|
||||
|
||||
$this->artisan('nova:recalculate-heat')
|
||||
->assertSuccessful();
|
||||
|
||||
$stat = ArtworkStats::where('artwork_id', $artwork->id)->first();
|
||||
expect((float) $stat->heat_score)->toBeGreaterThan(0);
|
||||
|
||||
// Verify delta values cached on stats
|
||||
expect((int) $stat->views_1h)->toBe(20); // 30 - 10
|
||||
expect((int) $stat->downloads_1h)->toBe(3); // 5 - 2
|
||||
expect((int) $stat->favourites_1h)->toBe(3); // 4 - 1
|
||||
expect((int) $stat->comments_1h)->toBe(2); // 2 - 0
|
||||
expect((int) $stat->shares_1h)->toBe(1); // 1 - 0
|
||||
});
|
||||
|
||||
it('handles negative deltas gracefully by clamping to zero', function () {
|
||||
$artwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
ArtworkStats::upsert([
|
||||
['artwork_id' => $artwork->id, 'views' => 0, 'downloads' => 0, 'favorites' => 0],
|
||||
], ['artwork_id']);
|
||||
|
||||
$prevHour = now()->startOfHour()->subHour();
|
||||
$currentHour = now()->startOfHour();
|
||||
|
||||
// Simulate counter reset: current < previous
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => $prevHour,
|
||||
'views_count' => 100,
|
||||
'downloads_count' => 50,
|
||||
'favourites_count' => 20,
|
||||
'comments_count' => 10,
|
||||
'shares_count' => 5,
|
||||
]);
|
||||
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => $currentHour,
|
||||
'views_count' => 50, // < prev
|
||||
'downloads_count' => 30, // < prev
|
||||
'favourites_count' => 10, // < prev
|
||||
'comments_count' => 5, // < prev
|
||||
'shares_count' => 2, // < prev
|
||||
]);
|
||||
|
||||
$this->artisan('nova:recalculate-heat')
|
||||
->assertSuccessful();
|
||||
|
||||
$stat = ArtworkStats::where('artwork_id', $artwork->id)->first();
|
||||
expect((float) $stat->heat_score)->toBe(0.0); // all deltas negative → clamped to 0
|
||||
expect((int) $stat->views_1h)->toBe(0);
|
||||
expect((int) $stat->downloads_1h)->toBe(0);
|
||||
});
|
||||
|
||||
// ─── Pruning Command ──────────────────────────────────────────────────────
|
||||
|
||||
it('nova:prune-metric-snapshots removes old data', function () {
|
||||
$artwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDays(30),
|
||||
]);
|
||||
|
||||
// Old snapshot (10 days ago)
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => now()->subDays(10)->startOfHour(),
|
||||
'views_count' => 50,
|
||||
'downloads_count' => 5,
|
||||
'favourites_count' => 2,
|
||||
'comments_count' => 0,
|
||||
'shares_count' => 0,
|
||||
]);
|
||||
|
||||
// Recent snapshot (1 hour ago)
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'bucket_hour' => now()->subHour()->startOfHour(),
|
||||
'views_count' => 100,
|
||||
'downloads_count' => 10,
|
||||
'favourites_count' => 5,
|
||||
'comments_count' => 1,
|
||||
'shares_count' => 0,
|
||||
]);
|
||||
|
||||
$this->artisan('nova:prune-metric-snapshots --keep-days=7')
|
||||
->assertSuccessful();
|
||||
|
||||
$remaining = ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count();
|
||||
expect($remaining)->toBe(1); // only the recent one survives
|
||||
});
|
||||
|
||||
// ─── Heat Formula Unit Check ───────────────────────────────────────────────
|
||||
|
||||
it('heat formula applies age factor correctly', function () {
|
||||
// Newer artwork should get higher heat than older one with same deltas
|
||||
$newArtwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subHours(1),
|
||||
'created_at' => now()->subHours(1),
|
||||
]);
|
||||
|
||||
$oldArtwork = createArtworkWithoutObserver([
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDays(30),
|
||||
'created_at' => now()->subDays(30),
|
||||
]);
|
||||
|
||||
$prevHour = now()->startOfHour()->subHour();
|
||||
$currentHour = now()->startOfHour();
|
||||
|
||||
foreach ([$newArtwork, $oldArtwork] as $art) {
|
||||
ArtworkStats::upsert([
|
||||
['artwork_id' => $art->id, 'views' => 0, 'downloads' => 0, 'favorites' => 0],
|
||||
], ['artwork_id']);
|
||||
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $art->id,
|
||||
'bucket_hour' => $prevHour,
|
||||
'views_count' => 0,
|
||||
'downloads_count' => 0,
|
||||
'favourites_count' => 0,
|
||||
'comments_count' => 0,
|
||||
'shares_count' => 0,
|
||||
]);
|
||||
|
||||
ArtworkMetricSnapshotHourly::create([
|
||||
'artwork_id' => $art->id,
|
||||
'bucket_hour' => $currentHour,
|
||||
'views_count' => 100,
|
||||
'downloads_count' => 10,
|
||||
'favourites_count' => 5,
|
||||
'comments_count' => 3,
|
||||
'shares_count' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->artisan('nova:recalculate-heat')->assertSuccessful();
|
||||
|
||||
$newStat = ArtworkStats::where('artwork_id', $newArtwork->id)->first();
|
||||
$oldStat = ArtworkStats::where('artwork_id', $oldArtwork->id)->first();
|
||||
|
||||
expect((float) $newStat->heat_score)->toBeGreaterThan(0);
|
||||
expect((float) $oldStat->heat_score)->toBeGreaterThan(0);
|
||||
// Newer artwork should have higher heat score due to age factor
|
||||
expect((float) $newStat->heat_score)->toBeGreaterThan((float) $oldStat->heat_score);
|
||||
});
|
||||
Reference in New Issue
Block a user