makeRequest(AiEditRequest::STATUS_PROCESSING); app(AiObservabilityService::class)->recordTerminalOutcome( $request, AiEditRequest::STATUS_SUCCEEDED, 1200, false, 'process' ); $bucket = now()->format('YmdH'); $prefix = sprintf('ai-editing:obs:tenant:%d:event:%d:hour:%s', $request->tenant_id, $request->event_id, $bucket); $this->assertSame(1, (int) Cache::get($prefix.':total')); $this->assertSame(1, (int) Cache::get($prefix.':succeeded')); $this->assertSame(1200, (int) Cache::get($prefix.':duration_total_ms')); } public function test_it_logs_failure_rate_alert_when_threshold_is_reached(): void { config([ 'ai-editing.observability.failure_rate_alert_threshold' => 0.5, 'ai-editing.observability.failure_rate_min_samples' => 1, ]); $request = $this->makeRequest(AiEditRequest::STATUS_PROCESSING); Log::spy(); app(AiObservabilityService::class)->recordTerminalOutcome( $request, AiEditRequest::STATUS_FAILED, 500, false, 'poll' ); Log::shouldHaveReceived('warning') ->withArgs(function (string $message, array $context): bool { return $message === 'AI failure-rate alert threshold reached' && isset($context['failure_rate']) && $context['failure_rate'] >= 0.5; }) ->once(); } private function makeRequest(string $status): AiEditRequest { $event = Event::factory()->create(['status' => 'published']); $photo = Photo::factory()->for($event)->create([ 'tenant_id' => $event->tenant_id, 'status' => 'approved', ]); $style = AiStyle::query()->create([ 'key' => 'obs-style', 'name' => 'Observability Style', 'provider' => 'runware', 'provider_model' => 'runware-default', 'is_active' => true, ]); return AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'style_id' => $style->id, 'provider' => 'runware', 'provider_model' => 'runware-default', 'status' => $status, 'safety_state' => 'pending', 'prompt' => 'Observability', 'idempotency_key' => 'obs-'.uniqid('', true), 'queued_at' => now()->subMinute(), 'started_at' => now()->subSeconds(30), ]); } }