disallow to disable guard during focus
docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s

This commit is contained in:
timka123 committed 2026-10-28 09:50:58 +03:00
1 parent 157a113c39
commit d280101bce
21 files changed
+481 -43

No files matched your search

+30
View File
@@ -125,6 +125,36 @@ def test_invalid_settings_are_rejected(client):
assert client.put("/api/settings", json={"timezone": "Mars/Olympus"}).status_code == 422
def test_switching_the_guard_off_mid_interval_is_a_409(client):
assert client.put("/api/settings", json={"blocker": {"enabled": True}}).status_code == 200
client.post("/api/timer/start", json={"kind": "work"})
assert client.get("/api/blocker/policy").json()["locked"] is True
refused = client.put("/api/settings", json={"blocker": {"enabled": False}})
assert refused.status_code == 409
assert "focus guard" in refused.json()["detail"]
# Turning it on, and editing anything else, are both still fine.
assert client.put("/api/settings", json={"blocker": {"enabled": True}}).status_code == 200
lists = client.put("/api/settings", json={"blocker": {"blocked_apps": ["steam"]}})
assert lists.status_code == 200
assert lists.json()["settings"]["blocker"]["enabled"] is True
client.post("/api/timer/complete", json={"auto_next": False})
assert client.put("/api/settings", json={"blocker": {"enabled": False}}).status_code == 200
def test_resetting_the_settings_cannot_unguard_a_live_interval(client):
client.put("/api/settings", json={"blocker": {"enabled": True}})
client.post("/api/timer/start", json={"kind": "short_break"})
assert client.post("/api/settings/reset").status_code == 409
client.post("/api/timer/complete", json={"auto_next": False})
assert client.post("/api/settings/reset").status_code == 200
def test_notification_test_endpoint_falls_back_to_the_log(client):
body = client.post("/api/notifications/test", json={}).json()
assert body["deliveries"][0]["channel"] == "log"
+177
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import pytest
from app.services import blocker_service, pomodoro, settings_service
@@ -189,3 +190,179 @@ def test_the_policy_carries_the_allow_masks_for_the_stub(session, settings):
assert result["allowed"] == ["music.youtube.com", "*.docs.google.com"]
assert "music.youtube.com" not in result["domains"]
assert "m.youtube.com" in result["all_domains"]
# --------------------------------------------------------------------------
# The lock: the guard will not be switched off mid-interval
#
# Only `apply_patch` and `reset_to_env` carry the check. `save_settings` stays an
# unguarded low-level writer on purpose — the env seed, the password CLI and the
# tests above all go through it — which is why these exercise the real write path.
# --------------------------------------------------------------------------
def _guarded(session, settings):
settings.blocker.enabled = True
settings_service.save_settings(session, settings)
def test_the_guard_cannot_be_switched_off_while_an_interval_runs(session, settings):
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
with pytest.raises(blocker_service.GuardLocked):
settings_service.apply_patch(session, {"blocker": {"enabled": False}})
# And nothing was written on the way out.
assert settings_service.load_settings(session).blocker.enabled is True
def test_pausing_is_not_an_escape_hatch(session, settings):
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
pomodoro.pause(session, settings)
with pytest.raises(blocker_service.GuardLocked):
settings_service.apply_patch(session, {"blocker": {"enabled": False}})
@pytest.mark.parametrize("kind", ["short_break", "long_break"])
@pytest.mark.parametrize("pause", [False, True])
def test_a_break_is_not_an_escape_hatch_either(session, settings, kind, pause):
"""The lock is kind-blind: `current_session` is the whole rule."""
_guarded(session, settings)
pomodoro.start(session, settings, kind=kind)
if pause:
pomodoro.pause(session, settings)
with pytest.raises(blocker_service.GuardLocked):
settings_service.apply_patch(session, {"blocker": {"enabled": False}})
def test_the_guard_can_always_be_switched_on_mid_interval(session, settings):
pomodoro.start(session, settings, kind="work")
updated = settings_service.apply_patch(session, {"blocker": {"enabled": True}})
assert updated.blocker.enabled is True
def test_the_guard_switches_off_freely_when_nothing_is_running(session, settings):
_guarded(session, settings)
updated = settings_service.apply_patch(session, {"blocker": {"enabled": False}})
assert updated.blocker.enabled is False
def test_the_guard_switches_off_once_the_interval_is_over(session, settings):
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
pomodoro.complete(session, settings, auto_next=False)
updated = settings_service.apply_patch(session, {"blocker": {"enabled": False}})
assert updated.blocker.enabled is False
def test_an_unrelated_blocker_patch_still_saves_mid_interval(session, settings):
"""What the web app's list editor sends — the regression the check is shaped for."""
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
updated = settings_service.apply_patch(
session,
{"blocker": {"blocked_domains": ["youtube.com"], "blocked_apps": ["steam"]}},
)
assert updated.blocker.blocked_domains == ["youtube.com"]
assert updated.blocker.enabled is True
def test_re_sending_an_already_off_guard_is_not_a_refusal(session, settings):
pomodoro.start(session, settings, kind="work")
updated = settings_service.apply_patch(session, {"blocker": {"enabled": False}})
assert updated.blocker.enabled is False
def test_a_full_document_echo_survives_a_live_interval(session, settings):
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
echo = settings_service.load_settings(session).model_dump(mode="json")
updated = settings_service.apply_patch(session, echo)
assert updated.blocker.enabled is True
def test_a_loosely_typed_off_is_still_an_off(session, settings):
"""`{"enabled": "no"}` coerces to False, which is why the check compares
validated documents rather than reading the patch's keys."""
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
with pytest.raises(blocker_service.GuardLocked):
settings_service.apply_patch(session, {"blocker": {"enabled": "no"}})
def test_the_policy_reports_the_lock(session, settings):
_guarded(session, settings)
assert blocker_service.policy(session, settings)["locked"] is False
assert blocker_service.policy(session, settings)["lock_reason"] == ""
pomodoro.start(session, settings, kind="work")
live = blocker_service.policy(session, settings)
assert live["locked"] is True
assert live["lock_reason"] == blocker_service.GUARD_LOCK_NOTE
pomodoro.pause(session, settings)
assert blocker_service.policy(session, settings)["locked"] is True
def test_a_guard_that_is_off_is_never_reported_locked(session, settings):
"""`locked` has "and the guard is on" folded in, so no client re-derives it."""
pomodoro.start(session, settings, kind="work")
assert blocker_service.policy(session, settings)["locked"] is False
def test_the_policy_and_the_predicate_agree(session, settings):
"""`policy()` re-expresses `guard_locked()` to reuse the row it already has;
these two must not drift."""
for enabled in (False, True):
settings.blocker.enabled = enabled
settings_service.save_settings(session, settings)
for kind in (None, "work", "short_break"):
if kind is not None:
pomodoro.start(session, settings, kind=kind)
assert blocker_service.policy(session, settings)["locked"] is (
blocker_service.guard_locked(session, settings)
)
if kind is not None:
pomodoro.abandon(session, settings, reason="test")
def test_scheduled_mode_outside_its_window_is_inactive_and_still_locked(session, settings):
"""The distinction `active` cannot express: nothing is blocked right now, and
the guard still must not be switched off before the window opens."""
settings.blocker.enabled = True
settings.blocker.mode = "scheduled"
settings.blocker.schedule = []
settings_service.save_settings(session, settings)
pomodoro.start(session, settings, kind="work")
result = blocker_service.policy(session, settings)
assert result["active"] is False
assert result["locked"] is True
with pytest.raises(blocker_service.GuardLocked):
settings_service.apply_patch(session, {"blocker": {"enabled": False}})
def test_a_reset_cannot_switch_the_guard_off_mid_interval(session, settings):
"""`settings_from_env` never touches the blocker subtree, so a reset always
lands on `enabled = False` — a second, quieter off-switch."""
_guarded(session, settings)
pomodoro.start(session, settings, kind="work")
with pytest.raises(blocker_service.GuardLocked):
settings_service.reset_to_env(session)
assert settings_service.load_settings(session).blocker.enabled is True
pomodoro.complete(session, settings, auto_next=False)
assert settings_service.reset_to_env(session).blocker.enabled is False
def test_a_reset_is_fine_while_an_interval_runs_if_the_guard_is_off(session, settings):
pomodoro.start(session, settings, kind="work")
assert settings_service.reset_to_env(session).blocker.enabled is False