mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 16:48:53 +01:00
workout lock: do not re-arm after tonight's evening night-lockdown (gatelock 0.10.0 night_lockdown)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31a2056d52
commit
aa27a8ece6
@ -134,7 +134,7 @@ repos:
|
||||
# direct URLs for one distribution, so any pin that drifts from
|
||||
# the manifest becomes an unresolvable hook environment rather
|
||||
# than merely an old one.
|
||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
|
||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.10.0#subdirectory=gatelock
|
||||
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
|
||||
- earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.13.2#subdirectory=earned_time
|
||||
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays
|
||||
@ -156,7 +156,7 @@ repos:
|
||||
# direct URLs for one distribution, so any pin that drifts from
|
||||
# the manifest becomes an unresolvable hook environment rather
|
||||
# than merely an old one.
|
||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
|
||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.10.0#subdirectory=gatelock
|
||||
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
|
||||
- earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.13.2#subdirectory=earned_time
|
||||
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays
|
||||
|
||||
@ -71,7 +71,7 @@ ensure_runtime_deps() {
|
||||
echo "WARNING: could not install $1 automatically" >&2
|
||||
}
|
||||
_pip_runtime_dep gatelock \
|
||||
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock"
|
||||
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.10.0#subdirectory=gatelock"
|
||||
_pip_runtime_dep crdt_sync \
|
||||
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync"
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ requires-python = ">=3.14"
|
||||
# crdt-sync: shared CRDT merge scheme + sync transports, also used by
|
||||
# diet_guard; here for phone-workout sync, Firebase RTDB only since 2026-10-09
|
||||
dependencies = [
|
||||
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock",
|
||||
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.10.0#subdirectory=gatelock",
|
||||
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync",
|
||||
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays",
|
||||
"earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.13.2#subdirectory=earned_time",
|
||||
|
||||
@ -6,7 +6,7 @@ coverage==7.16.2
|
||||
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
|
||||
earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.13.2#subdirectory=earned_time
|
||||
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays
|
||||
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
|
||||
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.10.0#subdirectory=gatelock
|
||||
mcp==2.3.0
|
||||
mypy==2.4.0
|
||||
pre-commit==4.7.0
|
||||
|
||||
@ -25,6 +25,7 @@ import sys
|
||||
from screen_locker._decision_log import LockDecision, record_decision
|
||||
from screen_locker._decision_reasons import reasons_extra
|
||||
from screen_locker._morning_session import has_workout_skip_today
|
||||
from screen_locker._night_lockdown import has_evening_lockdown_today
|
||||
from screen_locker._sick import stands_down_workout
|
||||
from screen_locker._weekly_check import has_weekly_minimum, is_relaxed_day
|
||||
|
||||
@ -71,6 +72,7 @@ class _ReasonsMixin:
|
||||
"wake_alarm_skip": has_workout_skip_today,
|
||||
"relaxed_day": is_relaxed_day,
|
||||
"weekly_minimum_met": lambda: has_weekly_minimum(self.log_file),
|
||||
"evening_lockdown_today": has_evening_lockdown_today,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ def explain_lock_decision(
|
||||
weekly_minimum_met: bool,
|
||||
relaxed_day: bool,
|
||||
wake_skip: bool = False,
|
||||
evening_lockdown: bool = False,
|
||||
now: datetime | None = None,
|
||||
degraded_sources: Sequence[DegradedSource] = (),
|
||||
) -> LockExplanation:
|
||||
@ -215,6 +216,17 @@ def explain_lock_decision(
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
result = _check(
|
||||
"evening_lockdown_today",
|
||||
fired=evening_lockdown,
|
||||
reason_true="Tonight's night lockdown already fired (>= 12:00 today).",
|
||||
reason_false="No evening night lockdown yet today.",
|
||||
terminal_reason="Night lockdown already happened tonight — the workout "
|
||||
"lock does not re-arm.",
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# Named for the conclusion; "..._pending_heat_check" read as "stuck".
|
||||
return _stage(
|
||||
fired=True,
|
||||
|
||||
70
screen_locker/_night_lockdown.py
Normal file
70
screen_locker/_night_lockdown.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""The workout lock does not re-arm after tonight's evening night-lockdown.
|
||||
|
||||
The predicate itself is ``gatelock.night_lockdown``, shared with
|
||||
leetcode-guard and book-guard (diet-guard deliberately does not opt in); this
|
||||
module only binds screen-locker's path, which the test suite redirects, and
|
||||
the ladder rung that honours it.
|
||||
|
||||
Why: once tonight's lockdown has fired, a late credit (``_late_credit``) can
|
||||
lift it -- and re-locking behind the workout gate the same evening would make
|
||||
a finished workout impossible to submit. Only an EVENING lock counts (local
|
||||
date today, >= 12:00); the 04:01 morning re-lock does not. Missing or garbage
|
||||
stamp means "no lockdown": the workout lock behaves exactly as before.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gatelock.night_lockdown import (
|
||||
NIGHT_LOCKDOWN_STAMP as _SHARED_STAMP,
|
||||
)
|
||||
from gatelock.night_lockdown import (
|
||||
evening_lockdown_today,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"EVENING_LOCKDOWN_REASON",
|
||||
"NIGHT_LOCKDOWN_STAMP",
|
||||
"has_evening_lockdown_today",
|
||||
"skip_after_evening_lockdown",
|
||||
"tonights_lockdown",
|
||||
]
|
||||
|
||||
NIGHT_LOCKDOWN_STAMP: Path = _SHARED_STAMP
|
||||
|
||||
EVENING_LOCKDOWN_REASON = "evening_lockdown_today"
|
||||
|
||||
|
||||
def tonights_lockdown(now: datetime | None = None) -> datetime | None:
|
||||
"""When tonight's evening lockdown locked, or None if it has not yet."""
|
||||
return evening_lockdown_today(NIGHT_LOCKDOWN_STAMP, now)
|
||||
|
||||
|
||||
def has_evening_lockdown_today() -> bool:
|
||||
"""Whether tonight's evening lockdown already happened. Read-only."""
|
||||
return tonights_lockdown() is not None
|
||||
|
||||
|
||||
def skip_after_evening_lockdown(record_skip: Callable[..., None]) -> bool:
|
||||
"""Skip enforcement once tonight's evening lockdown has fired.
|
||||
|
||||
Takes the locker's ``_record_skip`` (which exits the process) rather than
|
||||
the locker, like ``_lock_invariant``. Returns True when the skip was
|
||||
recorded, False when the lock may go ahead.
|
||||
"""
|
||||
locked_at = tonights_lockdown()
|
||||
if locked_at is None:
|
||||
return False
|
||||
record_skip(
|
||||
EVENING_LOCKDOWN_REASON,
|
||||
f"Tonight's night lockdown already locked at {locked_at:%H:%M}; the "
|
||||
"workout lock does not re-arm after it.",
|
||||
locked_at=locked_at.isoformat(),
|
||||
)
|
||||
return True
|
||||
@ -29,6 +29,7 @@ from screen_locker._decision_log import (
|
||||
from screen_locker._degraded_sources import degraded_sources
|
||||
from screen_locker._extra_benefits import process_week_transition
|
||||
from screen_locker._lock_invariant import refuse_to_lock_over_logged_workout
|
||||
from screen_locker._night_lockdown import skip_after_evening_lockdown
|
||||
from screen_locker._shutdown_base import (
|
||||
apply_flat_bonuses_if_new,
|
||||
reset_to_base_if_new_day,
|
||||
@ -157,7 +158,11 @@ class StartupChecksMixin(SyncMixin):
|
||||
# there is no banked "skip a workout" credit — that mechanic works
|
||||
# against the goal of maximizing weekly workouts, so it was removed.
|
||||
# A logged workout outranks every rung above, whatever their order.
|
||||
if refuse_to_lock_over_logged_workout(self.has_logged_today, self._record_skip):
|
||||
# Then: once tonight's lockdown fired (a late credit may have lifted
|
||||
# it), never re-arm behind the workout gate the same evening.
|
||||
if refuse_to_lock_over_logged_workout(
|
||||
self.has_logged_today, self._record_skip
|
||||
) or skip_after_evening_lockdown(self._record_skip):
|
||||
return
|
||||
heat = self._check_heat_skip_exit()
|
||||
# Nothing excused today: falling through here means the lock WILL be
|
||||
|
||||
@ -51,6 +51,7 @@ from screen_locker._extra_benefits import (
|
||||
)
|
||||
from screen_locker._log_io import load_workout_log
|
||||
from screen_locker._morning_session import has_workout_skip_today
|
||||
from screen_locker._night_lockdown import tonights_lockdown
|
||||
from screen_locker._sick import workout_standdown_dates
|
||||
from screen_locker._weekly_check import (
|
||||
COUNTED_WORKOUT_TYPES,
|
||||
@ -167,6 +168,7 @@ def gather_status(
|
||||
weekly_minimum_met=week.counted_count >= WEEKLY_WORKOUT_MINIMUM,
|
||||
relaxed_day=is_relaxed_day(today=today_local),
|
||||
wake_skip=has_workout_skip_today(),
|
||||
evening_lockdown=tonights_lockdown(instant) is not None,
|
||||
now=instant,
|
||||
)
|
||||
|
||||
|
||||
@ -170,3 +170,20 @@ def pre_ladder(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
Opt-in: tests that assert the ladder itself must keep the real cutover.
|
||||
"""
|
||||
monkeypatch.setattr("earned_time._ladder.LADDER_FROM", date.max)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_real_night_lockdown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Never let the lock chain read the machine's real night-lockdown stamp.
|
||||
|
||||
``/var/lib/night-lockdown/locked_at_epoch`` is world-readable, so without
|
||||
this every enforce-path test would skip the lock on any evening after the
|
||||
real lockdown fired -- and pass in the morning.
|
||||
"""
|
||||
import screen_locker._night_lockdown
|
||||
|
||||
monkeypatch.setattr(
|
||||
screen_locker._night_lockdown,
|
||||
"NIGHT_LOCKDOWN_STAMP",
|
||||
tmp_path / "locked_at_epoch",
|
||||
)
|
||||
|
||||
167
screen_locker/tests/test_night_lockdown_rung.py
Normal file
167
screen_locker/tests/test_night_lockdown_rung.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""The workout lock does not re-arm after tonight's evening night-lockdown.
|
||||
|
||||
``gatelock.night_lockdown`` decides "has the evening lockdown happened?";
|
||||
these tests pin screen-locker's binding of its path, the ladder rung that
|
||||
honours it, and the status view's mirror of that rung.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from screen_locker import _night_lockdown
|
||||
from screen_locker._compliance_state import explain_lock_decision
|
||||
from screen_locker._night_lockdown import (
|
||||
EVENING_LOCKDOWN_REASON,
|
||||
skip_after_evening_lockdown,
|
||||
tonights_lockdown,
|
||||
)
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
NOW = datetime(2026, 10, 17, 22, 30).astimezone()
|
||||
EVENING = datetime(2026, 10, 17, 21, 5).astimezone()
|
||||
|
||||
|
||||
def _stamp(when: str) -> None:
|
||||
"""Write the redirected stamp for a LOCAL wall-clock time."""
|
||||
epoch = int(datetime.fromisoformat(when).astimezone().timestamp())
|
||||
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text(f"{epoch}\n")
|
||||
|
||||
|
||||
class TestTonightsLockdown:
|
||||
"""The binding reads the (redirected) stamp through gatelock."""
|
||||
|
||||
def test_missing_stamp_is_no_lockdown(self) -> None:
|
||||
assert tonights_lockdown(NOW) is None
|
||||
|
||||
def test_evening_stamp_today_is_tonights_lockdown(self) -> None:
|
||||
_stamp("2026-10-17T21:05")
|
||||
assert tonights_lockdown(NOW) == EVENING
|
||||
|
||||
def test_morning_relock_today_is_not(self) -> None:
|
||||
_stamp("2026-10-17T04:01")
|
||||
assert tonights_lockdown(NOW) is None
|
||||
|
||||
def test_yesterdays_evening_is_not(self) -> None:
|
||||
_stamp("2026-10-16T21:05")
|
||||
assert tonights_lockdown(NOW) is None
|
||||
|
||||
def test_garbage_is_not(self) -> None:
|
||||
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text("not an epoch")
|
||||
assert tonights_lockdown(NOW) is None
|
||||
|
||||
|
||||
class TestSkipAfterEveningLockdown:
|
||||
"""The rung on its own."""
|
||||
|
||||
def test_no_lockdown_lets_the_lock_proceed(self) -> None:
|
||||
record_skip = MagicMock()
|
||||
with patch.object(_night_lockdown, "tonights_lockdown", return_value=None):
|
||||
assert skip_after_evening_lockdown(record_skip) is False
|
||||
record_skip.assert_not_called()
|
||||
|
||||
def test_tonights_lockdown_records_the_skip(self) -> None:
|
||||
record_skip = MagicMock()
|
||||
with patch.object(_night_lockdown, "tonights_lockdown", return_value=EVENING):
|
||||
assert skip_after_evening_lockdown(record_skip) is True
|
||||
reason, detail = record_skip.call_args.args
|
||||
assert reason == EVENING_LOCKDOWN_REASON == "evening_lockdown_today"
|
||||
assert "21:05" in detail
|
||||
assert record_skip.call_args.kwargs == {"locked_at": EVENING.isoformat()}
|
||||
|
||||
|
||||
def _fall_through_the_ladder(locker: object) -> ExitStack:
|
||||
"""Every rung above the new one falls through (see test_lock_invariant)."""
|
||||
stack = ExitStack()
|
||||
for patcher in (
|
||||
patch.object(locker, "_is_scheduled_skip_today", return_value=False),
|
||||
patch.object(locker, "_ingest_synced_manual_workouts"),
|
||||
patch.object(locker, "_auto_fill_week_runnerup_bonus"),
|
||||
patch.object(locker, "_check_today_state_exits", return_value=False),
|
||||
patch.object(locker, "has_logged_today", return_value=False),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker._startup_checks.has_weekly_minimum", return_value=False),
|
||||
patch("screen_locker._startup_checks.process_week_transition", return_value=[]),
|
||||
patch(
|
||||
"screen_locker._startup_checks.reset_to_base_if_new_day",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
stack.enter_context(patcher)
|
||||
return stack
|
||||
|
||||
|
||||
class TestLadder:
|
||||
"""``_check_non_verify_exits`` reaching the enforce point."""
|
||||
|
||||
def test_after_tonights_lockdown_the_lock_does_not_rearm(
|
||||
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch.object(_night_lockdown, "tonights_lockdown", return_value=EVENING),
|
||||
patch("screen_locker._startup_checks.record_decision") as record,
|
||||
patch.object(locker, "_check_heat_skip_exit") as heat,
|
||||
patch("screen_locker._startup_checks.apply_flat_bonuses_if_new") as bonus,
|
||||
_fall_through_the_ladder(locker),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
assert [c.args[0].reason for c in record.call_args_list] == [
|
||||
"evening_lockdown_today"
|
||||
]
|
||||
assert record.call_args.args[0].locked is False
|
||||
heat.assert_not_called()
|
||||
# Credits still land: the rung sits below the reset/bonus/ingest block.
|
||||
bonus.assert_called_once()
|
||||
mock_sys_exit.assert_called_once_with(0)
|
||||
|
||||
def test_morning_relock_still_enforces(
|
||||
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
_stamp(f"{datetime.now(tz=UTC).astimezone():%Y-%m-%d}T04:01")
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch("screen_locker._startup_checks.record_decision") as record,
|
||||
patch.object(locker, "_check_heat_skip_exit", return_value="skipped"),
|
||||
patch("screen_locker._startup_checks.apply_flat_bonuses_if_new"),
|
||||
_fall_through_the_ladder(locker),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
assert [c.args[0].reason for c in record.call_args_list] == ["enforced"]
|
||||
mock_sys_exit.assert_not_called()
|
||||
|
||||
def test_other_conditions_name_tonights_lockdown(
|
||||
self, mock_tk: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch.object(_night_lockdown, "tonights_lockdown", return_value=EVENING):
|
||||
also = locker._other_conditions("enforced")
|
||||
assert "evening_lockdown_today" in str(also)
|
||||
|
||||
|
||||
class TestStatusMirror:
|
||||
"""``explain_lock_decision`` names the rung instead of ``would_lock``."""
|
||||
|
||||
def _explain(self, tmp_path: Path, *, evening_lockdown: bool) -> object:
|
||||
return explain_lock_decision(
|
||||
log_file=tmp_path / "log.json",
|
||||
early_bird_pending_file=tmp_path / "early_bird_pending.json",
|
||||
weekly_minimum_met=False,
|
||||
relaxed_day=False,
|
||||
evening_lockdown=evening_lockdown,
|
||||
)
|
||||
|
||||
def test_tonights_lockdown_is_the_terminal_stage(self, tmp_path: Path) -> None:
|
||||
result = self._explain(tmp_path, evening_lockdown=True)
|
||||
assert result.stage == "evening_lockdown_today"
|
||||
assert result.fired is False
|
||||
|
||||
def test_without_it_the_chain_still_would_lock(self, tmp_path: Path) -> None:
|
||||
result = self._explain(tmp_path, evening_lockdown=False)
|
||||
assert result.stage == "would_lock"
|
||||
Loading…
Reference in New Issue
Block a user