mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:23:13 +02:00
Add tests and fix pre-commit issues across all projects
- C/lichess_random_engine, vocabulary_curve, misc/split, 1dvelocitysimulator, opening_learner: test suites added - CPP/miscelanious: tests added - TS/battery-status, champions_leauge_scores, two-inputs: tests added - python_pkg/fm24_searcher, wake_alarm: new packages added - Fix ruff/cppcheck/eslint/clang-format failures - Update .gitignore for C/C++ build artifacts
This commit is contained in:
parent
9c409a32cd
commit
f424be8fe5
@ -1,78 +1,19 @@
|
||||
"""HMAC-based integrity checking for workout log entries."""
|
||||
"""HMAC-based integrity checking — re-exports from shared package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
from python_pkg.shared.log_integrity import (
|
||||
HMAC_KEY_FILE,
|
||||
_generate_hmac_key,
|
||||
_load_hmac_key,
|
||||
compute_entry_hmac,
|
||||
verify_entry_hmac,
|
||||
)
|
||||
|
||||
from python_pkg.screen_locker._constants import HMAC_KEY_FILE
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_hmac_key() -> bytes | None:
|
||||
"""Load HMAC key from the root-owned key file.
|
||||
|
||||
Returns the key bytes, or None if the file cannot be read.
|
||||
"""
|
||||
try:
|
||||
return HMAC_KEY_FILE.read_bytes().strip()
|
||||
except OSError:
|
||||
_logger.warning("Cannot read HMAC key from %s", HMAC_KEY_FILE)
|
||||
return None
|
||||
|
||||
|
||||
def _generate_hmac_key() -> bytes | None:
|
||||
"""Generate a new HMAC key and write it to the key file.
|
||||
|
||||
The key file must be writable (requires root or setup script).
|
||||
Returns the new key bytes, or None on failure.
|
||||
"""
|
||||
key = secrets.token_bytes(32)
|
||||
try:
|
||||
HMAC_KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
HMAC_KEY_FILE.write_bytes(key)
|
||||
except OSError:
|
||||
_logger.warning("Cannot write HMAC key to %s", HMAC_KEY_FILE)
|
||||
return None
|
||||
return key
|
||||
|
||||
|
||||
def compute_entry_hmac(entry_data: dict[str, object]) -> str | None:
|
||||
"""Compute HMAC-SHA256 for a workout log entry.
|
||||
|
||||
Args:
|
||||
entry_data: The log entry dict (without the 'hmac' field).
|
||||
|
||||
Returns:
|
||||
Hex-encoded HMAC string, or None if the key is unavailable.
|
||||
"""
|
||||
key = _load_hmac_key()
|
||||
if key is None:
|
||||
return None
|
||||
payload = json.dumps(entry_data, sort_keys=True, separators=(",", ":"))
|
||||
return hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def verify_entry_hmac(entry: dict[str, object]) -> bool:
|
||||
"""Verify HMAC signature of a workout log entry.
|
||||
|
||||
Args:
|
||||
entry: The full log entry dict including the 'hmac' field.
|
||||
|
||||
Returns:
|
||||
True if the HMAC is valid, False if invalid or key unavailable.
|
||||
"""
|
||||
stored_hmac = entry.get("hmac")
|
||||
if not isinstance(stored_hmac, str):
|
||||
return False
|
||||
key = _load_hmac_key()
|
||||
if key is None:
|
||||
return False
|
||||
entry_without_hmac = {k: v for k, v in entry.items() if k != "hmac"}
|
||||
payload = json.dumps(entry_without_hmac, sort_keys=True, separators=(",", ":"))
|
||||
expected = hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(stored_hmac, expected)
|
||||
__all__ = [
|
||||
"HMAC_KEY_FILE",
|
||||
"_generate_hmac_key",
|
||||
"_load_hmac_key",
|
||||
"compute_entry_hmac",
|
||||
"verify_entry_hmac",
|
||||
]
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import calendar
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
@ -12,6 +13,11 @@ from python_pkg.screen_locker._constants import (
|
||||
SHUTDOWN_CONFIG_FILE,
|
||||
SICK_DAY_STATE_FILE,
|
||||
)
|
||||
from python_pkg.wake_alarm._constants import (
|
||||
ALARM_DAYS,
|
||||
RTCWAKE_BIN,
|
||||
WAKE_AFTER_HOURS,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -260,3 +266,73 @@ class ShutdownMixin:
|
||||
result.stdout.strip(),
|
||||
)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# rtcwake integration for weekend wake alarm
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _is_tomorrow_alarm_day() -> bool:
|
||||
"""Check if tomorrow is an alarm day."""
|
||||
tomorrow = datetime.now(tz=timezone.utc) + timedelta(days=1)
|
||||
return tomorrow.weekday() in ALARM_DAYS
|
||||
|
||||
@staticmethod
|
||||
def _compute_wake_timestamp() -> int:
|
||||
"""Compute the UTC epoch timestamp for the next wake alarm.
|
||||
|
||||
Returns:
|
||||
Epoch seconds WAKE_AFTER_HOURS from now.
|
||||
"""
|
||||
wake_time = datetime.now(tz=timezone.utc) + timedelta(
|
||||
hours=WAKE_AFTER_HOURS,
|
||||
)
|
||||
return calendar.timegm(wake_time.utctimetuple())
|
||||
|
||||
@staticmethod
|
||||
def _schedule_rtcwake() -> bool:
|
||||
"""Set rtcwake to power on the PC after WAKE_AFTER_HOURS.
|
||||
|
||||
Uses ``rtcwake -m no`` so the system can shut down normally while
|
||||
the RTC alarm remains set.
|
||||
|
||||
Returns:
|
||||
True if rtcwake was set successfully, False otherwise.
|
||||
"""
|
||||
wake_epoch = ShutdownMixin._compute_wake_timestamp()
|
||||
cmd = [
|
||||
"/usr/bin/sudo",
|
||||
RTCWAKE_BIN,
|
||||
"-m",
|
||||
"no",
|
||||
"-t",
|
||||
str(wake_epoch),
|
||||
]
|
||||
try:
|
||||
subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.SubprocessError as exc:
|
||||
_logger.warning("Failed to set rtcwake: %s", exc)
|
||||
return False
|
||||
_logger.info(
|
||||
"rtcwake set: PC will wake at epoch %d",
|
||||
wake_epoch,
|
||||
)
|
||||
return True
|
||||
|
||||
def schedule_wake_if_needed(self) -> bool:
|
||||
"""Schedule rtcwake if tomorrow is an alarm day.
|
||||
|
||||
Call this at shutdown time.
|
||||
|
||||
Returns:
|
||||
True if wake was scheduled, False if not needed or failed.
|
||||
"""
|
||||
if not self._is_tomorrow_alarm_day():
|
||||
_logger.info("Tomorrow is not an alarm day — skipping rtcwake")
|
||||
return False
|
||||
return self._schedule_rtcwake()
|
||||
|
||||
@ -31,6 +31,7 @@ from python_pkg.screen_locker._log_integrity import (
|
||||
from python_pkg.screen_locker._phone_verification import PhoneVerificationMixin
|
||||
from python_pkg.screen_locker._shutdown import ShutdownMixin
|
||||
from python_pkg.screen_locker._ui_flows import UIFlowsMixin
|
||||
from python_pkg.wake_alarm._state import has_workout_skip_today
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
@ -92,6 +93,9 @@ class ScreenLocker(
|
||||
elif self.has_logged_today():
|
||||
_logger.info("Workout already logged today. Skipping screen lock.")
|
||||
sys.exit(0)
|
||||
elif has_workout_skip_today():
|
||||
_logger.info("Wake alarm earned workout skip. Skipping screen lock.")
|
||||
sys.exit(0)
|
||||
self.root = tk.Tk()
|
||||
title_suffix = (
|
||||
" [VERIFY]" if verify_only else (" [DEMO MODE]" if demo_mode else "")
|
||||
|
||||
@ -15,6 +15,8 @@ from python_pkg.screen_locker._log_integrity import (
|
||||
verify_entry_hmac,
|
||||
)
|
||||
|
||||
_HMAC_KEY_FILE_PATH = "python_pkg.shared.log_integrity.HMAC_KEY_FILE"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@ -27,7 +29,7 @@ class TestLoadHmacKey:
|
||||
key_file = tmp_path / "hmac.key"
|
||||
key_file.write_bytes(b"secret_key_bytes")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
result = _load_hmac_key()
|
||||
@ -37,7 +39,7 @@ class TestLoadHmacKey:
|
||||
"""Test returns None when key file doesn't exist."""
|
||||
key_file = tmp_path / "nonexistent.key"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
result = _load_hmac_key()
|
||||
@ -51,7 +53,7 @@ class TestGenerateHmacKey:
|
||||
"""Test key generation creates file with 32-byte key."""
|
||||
key_file = tmp_path / "subdir" / "hmac.key"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
result = _generate_hmac_key()
|
||||
@ -62,7 +64,7 @@ class TestGenerateHmacKey:
|
||||
def test_returns_none_on_write_failure(self) -> None:
|
||||
"""Test returns None when file cannot be written."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
) as mock_path:
|
||||
mock_path.parent.mkdir.side_effect = OSError("permission denied")
|
||||
result = _generate_hmac_key()
|
||||
@ -79,7 +81,7 @@ class TestComputeEntryHmac:
|
||||
key_file.write_bytes(key)
|
||||
entry = {"timestamp": "2025-01-01T00:00:00", "workout_data": {"type": "test"}}
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
result = compute_entry_hmac(entry)
|
||||
@ -93,7 +95,7 @@ class TestComputeEntryHmac:
|
||||
"""Test returns None when key file is missing."""
|
||||
key_file = tmp_path / "nonexistent.key"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
result = compute_entry_hmac({"data": "test"})
|
||||
@ -114,7 +116,7 @@ class TestVerifyEntryHmac:
|
||||
entry = {**entry_data, "hmac": correct_hmac}
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
assert verify_entry_hmac(entry) is True
|
||||
@ -126,7 +128,7 @@ class TestVerifyEntryHmac:
|
||||
entry = {"timestamp": "2025-01-01", "hmac": "wrong_hmac_value"}
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
assert verify_entry_hmac(entry) is False
|
||||
@ -146,7 +148,7 @@ class TestVerifyEntryHmac:
|
||||
key_file = tmp_path / "nonexistent.key"
|
||||
entry = {"timestamp": "2025-01-01", "hmac": "some_hmac"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker._log_integrity.HMAC_KEY_FILE",
|
||||
_HMAC_KEY_FILE_PATH,
|
||||
key_file,
|
||||
):
|
||||
assert verify_entry_hmac(entry) is False
|
||||
|
||||
188
screen_locker/tests/test_wake_shutdown.py
Normal file
188
screen_locker/tests/test_wake_shutdown.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Tests for rtcwake integration in ShutdownMixin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestIsTomorrowAlarmDay:
|
||||
"""Tests for _is_tomorrow_alarm_day."""
|
||||
|
||||
def test_sunday_evening_means_monday_alarm(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Sunday evening → Monday is alarm day (weekday=0)."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Sunday 2026-04-12 → tomorrow Monday
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=timezone.utc)
|
||||
mock_dt.side_effect = datetime
|
||||
from datetime import timedelta
|
||||
|
||||
# Ensure timedelta works
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
):
|
||||
assert locker._is_tomorrow_alarm_day() is True
|
||||
|
||||
def test_monday_evening_is_not_alarm_next(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Monday evening → Tuesday is NOT an alarm day."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# Monday 2026-04-13 → tomorrow Tuesday (weekday=1)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
) as mock_dt,
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
),
|
||||
):
|
||||
mock_dt.now.return_value = datetime(2026, 4, 13, 23, 0, tzinfo=timezone.utc)
|
||||
mock_dt.side_effect = datetime
|
||||
assert locker._is_tomorrow_alarm_day() is False
|
||||
|
||||
def test_thursday_evening_friday_is_alarm(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Thursday evening → Friday is alarm day (weekday=4)."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# Thursday 2026-04-16 → tomorrow Friday (weekday=4)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
) as mock_dt,
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
),
|
||||
):
|
||||
mock_dt.now.return_value = datetime(2026, 4, 16, 23, 0, tzinfo=timezone.utc)
|
||||
mock_dt.side_effect = datetime
|
||||
assert locker._is_tomorrow_alarm_day() is True
|
||||
|
||||
|
||||
class TestScheduleRtcwake:
|
||||
"""Tests for _schedule_rtcwake."""
|
||||
|
||||
def test_success(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Successful rtcwake call returns True."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
assert locker._schedule_rtcwake() is True
|
||||
mock_run.assert_called_once()
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "rtcwake" in cmd[1]
|
||||
|
||||
def test_failure_returns_false(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Failed rtcwake call returns False."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
import subprocess
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
side_effect=subprocess.SubprocessError("rtcwake failed"),
|
||||
):
|
||||
assert locker._schedule_rtcwake() is False
|
||||
|
||||
|
||||
class TestScheduleWakeIfNeeded:
|
||||
"""Tests for schedule_wake_if_needed."""
|
||||
|
||||
def test_skips_when_not_alarm_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Returns False when tomorrow is not an alarm day."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch.object(locker, "_is_tomorrow_alarm_day", return_value=False):
|
||||
assert locker.schedule_wake_if_needed() is False
|
||||
|
||||
def test_schedules_when_alarm_day(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Returns True when tomorrow is an alarm day and rtcwake succeeds."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch.object(locker, "_is_tomorrow_alarm_day", return_value=True),
|
||||
patch.object(locker, "_schedule_rtcwake", return_value=True),
|
||||
):
|
||||
assert locker.schedule_wake_if_needed() is True
|
||||
|
||||
def test_returns_false_when_rtcwake_fails(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Returns False when rtcwake call fails."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch.object(locker, "_is_tomorrow_alarm_day", return_value=True),
|
||||
patch.object(locker, "_schedule_rtcwake", return_value=False),
|
||||
):
|
||||
assert locker.schedule_wake_if_needed() is False
|
||||
|
||||
|
||||
class TestComputeWakeTimestamp:
|
||||
"""Tests for _compute_wake_timestamp."""
|
||||
|
||||
def test_returns_future_epoch(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Wake timestamp is roughly 8 hours from now."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
import time
|
||||
|
||||
now = int(time.time())
|
||||
wake = locker._compute_wake_timestamp()
|
||||
# Should be ~8 hours ahead (within 60 second tolerance)
|
||||
expected = now + 8 * 3600
|
||||
assert abs(wake - expected) < 60
|
||||
83
screen_locker/tests/test_wake_skip.py
Normal file
83
screen_locker/tests/test_wake_skip.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Tests for wake alarm skip integration in screen_lock.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestWakeSkipIntegration:
|
||||
"""Tests for workout skip via wake alarm in screen locker init."""
|
||||
|
||||
def test_exits_when_wake_skip_active(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Screen locker exits if wake alarm granted workout skip today."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, has_logged=False)
|
||||
|
||||
mock_sys_exit.assert_called_once_with(0)
|
||||
|
||||
def test_does_not_exit_when_no_wake_skip(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Screen locker proceeds normally if no wake skip active."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=False,
|
||||
):
|
||||
locker = create_locker(mock_tk, tmp_path, has_logged=False)
|
||||
|
||||
mock_sys_exit.assert_not_called()
|
||||
assert locker is not None
|
||||
|
||||
def test_logged_today_takes_precedence(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""has_logged_today exits before wake skip is even checked."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, has_logged=True)
|
||||
|
||||
# Exits because has_logged_today, not because of wake skip
|
||||
mock_sys_exit.assert_called_once_with(0)
|
||||
|
||||
def test_verify_only_mode_ignores_wake_skip(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""verify_only mode checks sick day log, not wake skip."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(
|
||||
mock_tk,
|
||||
tmp_path,
|
||||
verify_only=True,
|
||||
is_sick_day_log=True,
|
||||
)
|
||||
|
||||
# In verify_only mode, exits don't happen from wake skip path
|
||||
mock_sys_exit.assert_not_called()
|
||||
Loading…
Reference in New Issue
Block a user