mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
Cover the headless dispatch and the degraded-source annotation
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Two gaps against the 100% bar. The first is mine: --log-manual-workout's dispatch branch had no test, so nothing checked that the flag's own name is stripped before the args are forwarded, or that the exit code is the logger's. The second predates this work. `_record_decision` annotates the DECISION line with any backend it could not read, which is the thing that distinguishes "weekly=0/5, measured" from "weekly=0/5, unreadable" -- the exact confusion that hid the 2026-08 outage -- and that branch was never exercised. Now both directions are: named when a source is degraded, absent when none are. test_sync_degradation.py was at the 250-line cap, so the new class lives in test_sync_degradation_part2.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWq9MRemX9ZWvNmnhNWUP2
This commit is contained in:
parent
9131c8086e
commit
fa8fa208d8
@ -173,3 +173,51 @@ class TestScreenLockDelegation:
|
||||
"""main is the module's public entry point."""
|
||||
assert _cli.__all__ == ["main"]
|
||||
assert callable(_cli.main)
|
||||
|
||||
|
||||
class TestLogManualWorkoutMode:
|
||||
"""--log-manual-workout hands everything after the flag to the logger.
|
||||
|
||||
Dispatch only: the field rules themselves are covered by
|
||||
``test_manual_cli.py`` against the real validator.
|
||||
"""
|
||||
|
||||
def test_forwards_the_remaining_args_and_exits_with_its_code(self) -> None:
|
||||
"""The flag's own name is not passed on, and the exit code is used."""
|
||||
seen: list[tuple[object, list[str]]] = []
|
||||
|
||||
def _fake_run(log_file: object, argv: list[str]) -> int:
|
||||
"""Record the call and report a refusal."""
|
||||
seen.append((log_file, list(argv)))
|
||||
return 1
|
||||
|
||||
with (
|
||||
patch.object(_cli.logging, "basicConfig"),
|
||||
patch.object(_cli, "run_manual_log", _fake_run),
|
||||
patch.object(_cli.sys, "exit") as sys_exit,
|
||||
):
|
||||
_cli.main(
|
||||
_FakeLocker,
|
||||
["screen_lock.py", "--log-manual-workout", "--sport", "other"],
|
||||
)
|
||||
|
||||
(log_file, argv) = seen[0]
|
||||
assert argv == ["--sport", "other"]
|
||||
assert log_file.name == "log.json"
|
||||
sys_exit.assert_called_once_with(1)
|
||||
|
||||
def test_exits_before_reaching_the_lock_screen(self) -> None:
|
||||
"""A successful log exits 0 rather than falling through to the UI.
|
||||
|
||||
``sys.exit`` is a no-op under the suite-wide fixture, so this asserts
|
||||
on the exit call: in the real process that call is what stops the
|
||||
headless path from continuing on to build a lock screen.
|
||||
"""
|
||||
with (
|
||||
patch.object(_cli.logging, "basicConfig"),
|
||||
patch.object(_cli, "run_manual_log", lambda *_: 0),
|
||||
patch.object(_cli.sys, "exit") as sys_exit,
|
||||
):
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--log-manual-workout"])
|
||||
|
||||
assert sys_exit.call_args_list[0].args == (0,)
|
||||
|
||||
73
screen_locker/tests/test_sync_degradation_part2.py
Normal file
73
screen_locker/tests/test_sync_degradation_part2.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""The DECISION line must name a backend it could not read.
|
||||
|
||||
Split from ``test_sync_degradation.py`` to keep both files under the 250-line
|
||||
cap; same subject, which is that "could not check" and "did not train" are
|
||||
different claims and only one of them justifies locking the screen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from screen_locker import _startup_checks, _sync_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestDecisionLineNamesTheDeadBackend:
|
||||
"""A degraded read must be named on the DECISION line itself.
|
||||
|
||||
A warning logged 90 seconds earlier is not enough: the line that says
|
||||
``weekly=0/5`` has to say whether that zero was measured or merely
|
||||
unreadable, or a reader tracing the journal draws the wrong conclusion
|
||||
about the user (see this module's docstring).
|
||||
"""
|
||||
|
||||
def _locker(self, tmp_path: Path) -> object:
|
||||
"""A StartupChecksMixin with __init__ bypassed, as the CLI builds it."""
|
||||
locker = object.__new__(_startup_checks.StartupChecksMixin)
|
||||
locker.log_file = tmp_path / "log.json"
|
||||
# Stubbed: the ladder annotation is its own concern, tested elsewhere.
|
||||
locker._other_conditions = lambda _reason: {}
|
||||
return locker
|
||||
|
||||
def test_annotates_the_decision_with_the_unreadable_source(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The dead backend's name reaches the recorded decision."""
|
||||
recorded: list[object] = []
|
||||
monkeypatch.setattr(
|
||||
_startup_checks,
|
||||
"degraded_sources",
|
||||
lambda: (_sync_client.DegradedSource("firebase", "HTTP 401"),),
|
||||
)
|
||||
monkeypatch.setattr(_startup_checks, "count_weekly_workouts", lambda _: 0)
|
||||
monkeypatch.setattr(
|
||||
_startup_checks, "record_decision", lambda d, **_: recorded.append(d)
|
||||
)
|
||||
|
||||
self._locker(tmp_path)._record_decision(
|
||||
locked=False, reason="weekly_minimum_met", detail=""
|
||||
)
|
||||
|
||||
assert recorded[0].extra["unreadable_sources"] == "firebase"
|
||||
|
||||
def test_healthy_sources_add_no_annotation(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""With every backend answering, the line stays unannotated."""
|
||||
recorded: list[object] = []
|
||||
monkeypatch.setattr(_startup_checks, "degraded_sources", tuple)
|
||||
monkeypatch.setattr(_startup_checks, "count_weekly_workouts", lambda _: 0)
|
||||
monkeypatch.setattr(
|
||||
_startup_checks, "record_decision", lambda d, **_: recorded.append(d)
|
||||
)
|
||||
|
||||
self._locker(tmp_path)._record_decision(
|
||||
locked=False, reason="weekly_minimum_met", detail=""
|
||||
)
|
||||
|
||||
assert "unreadable_sources" not in recorded[0].extra
|
||||
Loading…
Reference in New Issue
Block a user