shouldReceive('check')->once()->andReturn($healthyResult); // WHEN $job->handle( $healthChecker, app(Mailer::class), app(HealthAlertMailConfig::class), app(LoggerInterface::class), ); // THEN expect(File::exists($tempStateFile))->toBeTrue(); $saved = json_decode((string) File::get($tempStateFile), true); expect($saved['status'])->toBe('healthy'); Mail::assertNothingQueued(); }); it('sends degraded alert email on initial run when unhealthy', function () use (&$tempStateFile): void { // GIVEN $job = new HealthCheckJob($tempStateFile); $healthChecker = mock(HealthChecker::class); $unhealthyResult = new HealthCheckResult( status: HealthStatus::UNHEALTHY, timestamp: Utc::now(), checks: new HealthChecks( databaseDefault: new DatabaseCheckResult(ComponentStatus::DOWN, 'sqlite', 'Connection refused'), databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), cache: new CacheCheckResult(ComponentStatus::UP, 'file'), mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), ), ); $healthChecker->shouldReceive('check')->once()->andReturn($unhealthyResult); // WHEN $job->handle( $healthChecker, app(Mailer::class), app(HealthAlertMailConfig::class), app(LoggerInterface::class), ); // THEN expect(File::exists($tempStateFile))->toBeTrue(); $saved = json_decode((string) File::get($tempStateFile), true); expect($saved['status'])->toBe('unhealthy'); Mail::assertQueued(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool { return $mail->status === HealthStatus::UNHEALTHY && $mail->previousStatus === null; }); }); it('sends alert when status transitions from healthy to unhealthy', function () use (&$tempStateFile): void { // GIVEN: existing healthy state File::put($tempStateFile, (string) json_encode([ 'status' => 'healthy', 'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339), ])); $job = new HealthCheckJob($tempStateFile); $healthChecker = mock(HealthChecker::class); $unhealthyResult = new HealthCheckResult( status: HealthStatus::UNHEALTHY, timestamp: Utc::now(), checks: new HealthChecks( databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), databaseArcadia: new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'DB down'), cache: new CacheCheckResult(ComponentStatus::UP, 'file'), mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), ), ); $healthChecker->shouldReceive('check')->once()->andReturn($unhealthyResult); // WHEN $job->handle( $healthChecker, app(Mailer::class), app(HealthAlertMailConfig::class), app(LoggerInterface::class), ); // THEN Mail::assertQueued(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool { return $mail->status === HealthStatus::UNHEALTHY && $mail->previousStatus === HealthStatus::HEALTHY; }); }); it('sends recovery alert when status transitions from unhealthy to healthy', function () use (&$tempStateFile): void { // GIVEN: existing unhealthy state File::put($tempStateFile, (string) json_encode([ 'status' => 'unhealthy', 'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339), ])); $job = new HealthCheckJob($tempStateFile); $healthChecker = mock(HealthChecker::class); $healthyResult = new HealthCheckResult( status: HealthStatus::HEALTHY, timestamp: Utc::now(), checks: new HealthChecks( databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), cache: new CacheCheckResult(ComponentStatus::UP, 'file'), mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), ), ); $healthChecker->shouldReceive('check')->once()->andReturn($healthyResult); // WHEN $job->handle( $healthChecker, app(Mailer::class), app(HealthAlertMailConfig::class), app(LoggerInterface::class), ); // THEN Mail::assertQueued(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool { return $mail->status === HealthStatus::HEALTHY && $mail->previousStatus === HealthStatus::UNHEALTHY; }); }); it('does not send email when status remains unchanged', function () use (&$tempStateFile): void { // GIVEN: existing healthy state File::put($tempStateFile, (string) json_encode([ 'status' => 'healthy', 'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339), ])); $job = new HealthCheckJob($tempStateFile); $healthChecker = mock(HealthChecker::class); $healthyResult = new HealthCheckResult( status: HealthStatus::HEALTHY, timestamp: Utc::now(), checks: new HealthChecks( databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), cache: new CacheCheckResult(ComponentStatus::UP, 'file'), mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), ), ); $healthChecker->shouldReceive('check')->once()->andReturn($healthyResult); // WHEN $job->handle( $healthChecker, app(Mailer::class), app(HealthAlertMailConfig::class), app(LoggerInterface::class), ); // THEN Mail::assertNothingQueued(); }); });