|
25 | 25 | _gen_subdaily_gap_schedule_phase, |
26 | 26 | _gen_subdaily_regular, |
27 | 27 | _gen_training_only, |
| 28 | + _gen_twice_daily_outage, |
28 | 29 | _gen_weekly_early, |
29 | 30 | _run_scenario, |
30 | 31 | ) |
@@ -472,3 +473,48 @@ def test_recovery_passes(self, results: list[ScenarioPoint]) -> None: |
472 | 473 | assert post_recovery[0].result_code == 0 |
473 | 474 | # Second update after recovery should pass |
474 | 475 | assert post_recovery[1].result_code == 1 |
| 476 | + |
| 477 | + |
| 478 | +# ─── Scenario 9: Twice-Daily Outage (full-week schedule) ──────────── |
| 479 | + |
| 480 | + |
| 481 | +class Test_TwiceDailyOutage: |
| 482 | + """Updates every 12h around the clock for 5 weeks, then updates stop. |
| 483 | +
|
| 484 | + Full-week "daily"-frequency schedule: no excluded days and no sub-daily |
| 485 | + window, so the staleness threshold must come from the first-pass gap |
| 486 | + computation. With a 720-min median gap, staleness = 720 * 0.85 = 612 min, |
| 487 | + so the first missed check (gap 720 min) flags Late. |
| 488 | + """ |
| 489 | + |
| 490 | + @pytest.fixture(scope="class") |
| 491 | + def results(self) -> list[ScenarioPoint]: |
| 492 | + rows = _gen_twice_daily_outage() |
| 493 | + return _run_scenario(rows, PredictSensitivity.medium, exclude_weekends=False, tz="UTC") |
| 494 | + |
| 495 | + def test_schedule_goes_active_all_days(self, results: list[ScenarioPoint]) -> None: |
| 496 | + sched = _schedule(_updates(results)[-1]) |
| 497 | + assert sched is not None |
| 498 | + assert sched.get("schedule_stage") == "active" |
| 499 | + assert sched.get("active_days") == [0, 1, 2, 3, 4, 5, 6] |
| 500 | + |
| 501 | + def test_staleness_stored_once_active(self, results: list[ScenarioPoint]) -> None: |
| 502 | + last_update = _updates(results)[-1] |
| 503 | + assert last_update.staleness == pytest.approx(720 * 0.85) |
| 504 | + |
| 505 | + def test_no_anomalies_before_outage(self, results: list[ScenarioPoint]) -> None: |
| 506 | + outage_start = pd.Timestamp("2025-11-10") |
| 507 | + assert all(p.timestamp >= outage_start for p in _anomalies(results)) |
| 508 | + |
| 509 | + def test_first_missed_check_flags_late(self, results: list[ScenarioPoint]) -> None: |
| 510 | + anomalies = _anomalies(results) |
| 511 | + assert len(anomalies) > 0 |
| 512 | + first = anomalies[0] |
| 513 | + assert first.timestamp == pd.Timestamp("2025-11-10 00:00") |
| 514 | + assert first.value == 720 |
| 515 | + |
| 516 | + def test_all_missed_checks_flag(self, results: list[ScenarioPoint]) -> None: |
| 517 | + outage_start = pd.Timestamp("2025-11-10") |
| 518 | + missed_checks = [p for p in results if p.timestamp >= outage_start] |
| 519 | + assert len(missed_checks) == 4 |
| 520 | + assert all(p.result_code == 0 for p in missed_checks) |
0 commit comments