Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
165 lines
5.1 KiB
PHP
165 lines
5.1 KiB
PHP
<?php
|
|
|
|
use App\Models\ArtworkMetricSnapshotHourly;
|
|
|
|
it('prunes snapshots older than keep-days and keeps recent ones', function () {
|
|
$artwork = createArtworkWithoutObserver([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(40),
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(40)->startOfHour(),
|
|
'views_count' => 10,
|
|
'downloads_count' => 1,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(10)->startOfHour(),
|
|
'views_count' => 20,
|
|
'downloads_count' => 2,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subHour()->startOfHour(),
|
|
'views_count' => 30,
|
|
'downloads_count' => 3,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --keep-days=30 --sleep-ms=0')
|
|
->assertSuccessful();
|
|
|
|
$hours = ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)
|
|
->orderBy('bucket_hour')
|
|
->pluck('views_count')
|
|
->all();
|
|
|
|
expect($hours)->toBe([20, 30]);
|
|
});
|
|
|
|
it('deletes in bounded batches and is idempotent', function () {
|
|
$artwork = createArtworkWithoutObserver([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(20),
|
|
]);
|
|
|
|
foreach (range(1, 5) as $daysAgo) {
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays($daysAgo + 10)->startOfHour(),
|
|
'views_count' => $daysAgo,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
}
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --keep-days=7 --chunk=2 --sleep-ms=0')
|
|
->assertSuccessful();
|
|
|
|
expect(ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count())->toBe(0);
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --keep-days=7 --chunk=2 --sleep-ms=0')
|
|
->assertSuccessful();
|
|
|
|
expect(ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count())->toBe(0);
|
|
});
|
|
|
|
it('dry-run does not delete rows', function () {
|
|
$artwork = createArtworkWithoutObserver([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(20),
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(14)->startOfHour(),
|
|
'views_count' => 1,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --keep-days=7 --dry-run')
|
|
->assertSuccessful();
|
|
|
|
expect(ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count())->toBe(1);
|
|
});
|
|
|
|
it('uses config retention when keep-days is omitted', function () {
|
|
config(['metrics.hourly_snapshot_retention_days' => 7]);
|
|
|
|
$artwork = createArtworkWithoutObserver([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(20),
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(10)->startOfHour(),
|
|
'views_count' => 1,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(2)->startOfHour(),
|
|
'views_count' => 2,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --sleep-ms=0')
|
|
->assertSuccessful();
|
|
|
|
expect(ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count())->toBe(1);
|
|
});
|
|
|
|
it('honours max-batches so a run cannot delete the whole table at once', function () {
|
|
$artwork = createArtworkWithoutObserver([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(20),
|
|
]);
|
|
|
|
foreach (range(1, 4) as $n) {
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artwork->id,
|
|
'bucket_hour' => now()->subDays(10 + $n)->startOfHour(),
|
|
'views_count' => $n,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
}
|
|
|
|
$this->artisan('nova:prune-metric-snapshots --keep-days=7 --chunk=1 --max-batches=2 --sleep-ms=0')
|
|
->assertSuccessful();
|
|
|
|
expect(ArtworkMetricSnapshotHourly::where('artwork_id', $artwork->id)->count())->toBe(2);
|
|
});
|