user = new User(['login' => 'n', 'tz' => 'UTC', 'quiet_start' => '22:00:00', 'quiet_end' => '09:00:00']); $this->windows = new WindowResolver(30, 10, [9, 14, 20]); } private function at(string $s): CarbonImmutable { return CarbonImmutable::parse($s, 'UTC'); } private function wholeDay(string $ymd): Due { $day = $this->at($ymd); return new Due($day, $day->addDay()); } public function test_outside_active_hours_there_is_no_window(): void { $due = $this->wholeDay('2026-09-30'); $this->assertNull($this->windows->current(EventState::Preparing, $this->user, $due, $this->at('2026-09-20 08:59'))); $this->assertNull($this->windows->current(EventState::Preparing, $this->user, $due, $this->at('2026-09-20 22:00'))); $this->assertNull($this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 23:00'))); } public function test_preparing_and_overdue_have_one_daily_window(): void { $due = $this->wholeDay('2026-09-30'); foreach ([EventState::Preparing, EventState::Overdue] as $state) { $w = $this->windows->current($state, $this->user, $due, $this->at('2026-09-20 12:34')); $this->assertSame('2026-09-20 09:00', $w->start->format('Y-m-d H:i')); $this->assertSame('2026-09-20 22:00', $w->end->format('Y-m-d H:i')); $this->assertTrue($w->fallbackAllowed); $this->assertSame('2026-09-20 21:30', $w->fallbackAt->format('Y-m-d H:i')); } } public function test_today_has_hourly_windows_with_fallback_only_in_listed_hours(): void { $due = $this->wholeDay('2026-09-30'); $w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 09:20')); $this->assertSame('2026-09-30 09:00', $w->start->format('Y-m-d H:i')); $this->assertSame('2026-09-30 10:00', $w->end->format('Y-m-d H:i')); $this->assertTrue($w->fallbackAllowed); $this->assertSame('2026-09-30 09:50', $w->fallbackAt->format('Y-m-d H:i')); $w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:05')); $this->assertSame('2026-09-30 11:00', $w->start->format('Y-m-d H:i')); $this->assertFalse($w->fallbackAllowed); $w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 21:59')); $this->assertSame('2026-09-30 22:00', $w->end->format('Y-m-d H:i')); } public function test_today_window_is_truncated_by_due_moment_and_becomes_fallback(): void { $day = $this->at('2026-09-30'); $due = new Due($day, $day->setTime(9, 30)); $w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 09:05')); $this->assertSame('2026-09-30 09:30', $w->end->format('Y-m-d H:i')); $this->assertSame('2026-09-30 09:20', $w->fallbackAt->format('Y-m-d H:i')); $due = new Due($day, $day->setTime(11, 15)); $w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:02')); $this->assertSame('2026-09-30 11:15', $w->end->format('Y-m-d H:i')); $this->assertTrue($w->fallbackAllowed, 'обрезанное окно всегда fallback-окно'); $this->assertSame('2026-09-30 11:05', $w->fallbackAt->format('Y-m-d H:i')); } public function test_no_window_once_due_moment_passed(): void { $day = $this->at('2026-09-30'); $due = new Due($day, $day->setTime(11, 15)); $this->assertNull($this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:20'))); } }