screen-locker/screen_locker/tests/test_decision_trail.py
Krzysztof kuhy Rudnicki 1d5965b9a7
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 (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Close the four gaps that let a decided lock never reach the screen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 15:24:45 +02:00

129 lines
4.8 KiB
Python

"""Tests for _decision_trail: collapsing repeats so a storm cannot wipe history.
On 2026-08-30 the locker had no X server at 02:00, crashed, was restarted by
``Restart=on-failure`` every ~6.3s, and wrote ~1693 identical records in three
hours. At ``DECISION_LOG_MAX_ENTRIES = 3000`` that evicted everything behind
them: the tool built to make blind spots impossible erased its own history.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker._decision_trail import append_record
if TYPE_CHECKING:
from pathlib import Path
def _rows(target: Path) -> list[dict[str, object]]:
"""Return the trail as parsed records.
Args:
target: The trail file.
Returns:
One dict per line, oldest first.
"""
return [json.loads(line) for line in target.read_text().splitlines() if line]
def _record(index: int, *, reason: str = "enforced") -> dict[str, object]:
"""Build one record whose only varying field is its timestamp.
Args:
index: Distinguishes the timestamps.
reason: The decision reason.
Returns:
A record ready for append_record.
"""
return {
"timestamp": f"2026-08-30T02:{index:02d}:00+00:00",
"locked": True,
"reason": reason,
}
class TestCollapsing:
"""Consecutive identical events become one row, not N."""
def test_a_storm_of_identical_records_stays_one_row(self, tmp_path: Path) -> None:
"""The 1695-restart loop, in miniature."""
target = tmp_path / "d.jsonl"
for i in range(50):
append_record(_record(i), log_file=target)
rows = _rows(target)
assert len(rows) == 1
assert rows[0]["repeat_count"] == 50
def test_the_row_keeps_both_ends_of_the_streak(self, tmp_path: Path) -> None:
"""`timestamp` is when it began; `last_timestamp` is freshness."""
target = tmp_path / "d.jsonl"
append_record(_record(0), log_file=target)
append_record(_record(7), log_file=target)
row = _rows(target)[0]
assert row["timestamp"] == "2026-08-30T02:00:00+00:00"
assert row["last_timestamp"] == "2026-08-30T02:07:00+00:00"
def test_a_different_decision_opens_a_new_row(self, tmp_path: Path) -> None:
"""Collapsing must never hide a change of outcome."""
target = tmp_path / "d.jsonl"
append_record(_record(0), log_file=target)
append_record(_record(1, reason="weekly_minimum_met"), log_file=target)
assert len(_rows(target)) == 2
def test_only_consecutive_records_collapse(self, tmp_path: Path) -> None:
"""An intervening event ends the streak, as the history shows it must."""
target = tmp_path / "d.jsonl"
append_record(_record(0), log_file=target)
append_record(_record(1, reason="mode_makes_no_decision"), log_file=target)
append_record(_record(2), log_file=target)
rows = _rows(target)
assert len(rows) == 3
assert all("repeat_count" not in row for row in rows)
def test_a_changed_field_is_not_a_repeat(self, tmp_path: Path) -> None:
"""Everything but the timestamps must match to collapse."""
target = tmp_path / "d.jsonl"
append_record(_record(0), log_file=target)
append_record({**_record(1), "weekly_count": 4}, log_file=target)
assert len(_rows(target)) == 2
class TestCorruptTrail:
"""A trail that cannot be parsed is kept, loudly -- never overwritten."""
def test_an_unparsable_tail_starts_a_new_row(
self, tmp_path: Path, caplog: MagicMock
) -> None:
"""Overwriting a line we cannot read would destroy real history."""
target = tmp_path / "d.jsonl"
target.write_text("{truncated\n", encoding="utf-8")
append_record(_record(0), log_file=target)
lines = target.read_text().splitlines()
assert lines[0] == "{truncated"
assert len(lines) == 2
assert "unparsable" in caplog.text
def test_a_non_object_tail_starts_a_new_row(self, tmp_path: Path) -> None:
"""Valid JSON of the wrong shape is not a record to merge into."""
target = tmp_path / "d.jsonl"
target.write_text('["not", "a", "record"]\n', encoding="utf-8")
append_record(_record(0), log_file=target)
assert len(target.read_text().splitlines()) == 2
def test_a_write_failure_is_reported(
self, tmp_path: Path, caplog: MagicMock
) -> None:
"""A gap in the trail must itself be visible."""
target = tmp_path / "nested" / "d.jsonl"
with patch(
"screen_locker._decision_trail.Path.write_text",
side_effect=OSError("full"),
):
append_record(_record(0), log_file=target)
assert "durable trail now has a gap" in caplog.text