mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 13:27:41 +01:00
A real 1h57m workout on 2026-08-24 synced to Firebase and the locker still locked the screen. The trigger was auth: screen_locker was the only app with no cached firebase_auth.json, so it fell back to a stale shared password and got HTTP 400 every run. The defect was what it did next — an unreadable primary source was reported as "no workout" rather than "could not check", which is the same conflation that took 2026-06-12. Recover the session by borrowing a live refresh token from a sibling app, track degraded sources as state instead of a log line, and say so on the lock screen: name the backend, the error, and how stale the data is, so a sync fault reads as a sync fault. Also fix two bugs found while finishing the recovery: restore_and_bonus.py banked `existing + bonus` with no idempotence guard while finish_2026_08_24.sh advertised "safe to re-run" — a second run silently granted the compensation hours again. Guard on a durable `shutdown_bonus_granted_for` list; the bonus map alone cannot answer the question, since a week's total mixes earned streak hours with corrections. _extra_benefits._save_state rebuilt state from four hardcoded keys, so the next weekly rollover would have dropped that guard and re-armed the double-grant. Merge onto loaded state instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJmPLyT8NU4DzDJPV1KEX5
144 lines
4.9 KiB
Python
144 lines
4.9 KiB
Python
"""A dead Firebase credential must heal itself, not wait for a human.
|
|
|
|
On 2026-08-24 the PC could not read Firebase because ``screen_locker`` had no
|
|
cached session and the shared password no longer authenticated. Every sibling
|
|
app on the same machine -- ``diet_guard``, ``wake_alarm``, ``todo`` -- held a
|
|
live refresh token for the *same* account and project the whole time. The
|
|
credential was on disk, three directories away, and nothing looked for it.
|
|
|
|
Recovery is therefore local and needs no human: borrow a sibling's refresh
|
|
token, mint a session, and cache it under our own app name.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._credential_recovery import (
|
|
find_sibling_refresh_token,
|
|
recover_session,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _write_cache(path: Path, refresh_token: str, *, expires_at: str) -> None:
|
|
"""Write a credential cache shaped like ``FileCredentialStore``'s."""
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"id_token": "stale-id-token",
|
|
"refresh_token": refresh_token,
|
|
"expires_at": expires_at,
|
|
"local_id": "uid-1",
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
class TestFindSiblingRefreshToken:
|
|
"""Locating a live credential already present on this machine."""
|
|
|
|
def test_finds_a_sibling_apps_token(self, tmp_path: Path) -> None:
|
|
"""Any sibling app's cache is a valid source -- one account, one project."""
|
|
_write_cache(
|
|
tmp_path / "diet_guard" / "firebase_auth.json",
|
|
"refresh-abc",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
found = find_sibling_refresh_token(tmp_path, skip="screen_locker")
|
|
|
|
assert found == ("diet_guard", "refresh-abc")
|
|
|
|
def test_ignores_our_own_stale_cache(self, tmp_path: Path) -> None:
|
|
"""Our own dead cache is the thing being replaced, never the source."""
|
|
_write_cache(
|
|
tmp_path / "screen_locker" / "firebase_auth.json",
|
|
"refresh-ours",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
assert find_sibling_refresh_token(tmp_path, skip="screen_locker") is None
|
|
|
|
def test_skips_unreadable_and_malformed_caches(self, tmp_path: Path) -> None:
|
|
"""A corrupt sibling must not abort the search over the healthy ones."""
|
|
bad = tmp_path / "broken_app" / "firebase_auth.json"
|
|
bad.parent.mkdir(parents=True)
|
|
bad.write_text("{not json")
|
|
_write_cache(
|
|
tmp_path / "wake_alarm" / "firebase_auth.json",
|
|
"refresh-xyz",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
found = find_sibling_refresh_token(tmp_path, skip="screen_locker")
|
|
|
|
assert found == ("wake_alarm", "refresh-xyz")
|
|
|
|
def test_reports_nothing_when_no_sibling_has_one(self, tmp_path: Path) -> None:
|
|
"""No source is a real outcome; the caller must hear it, not guess."""
|
|
assert find_sibling_refresh_token(tmp_path, skip="screen_locker") is None
|
|
|
|
|
|
class TestRecoverSession:
|
|
"""Turning a borrowed refresh token into our own cached session."""
|
|
|
|
def test_recovers_and_reports_the_donor(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A successful recovery must say where the credential came from."""
|
|
_write_cache(
|
|
tmp_path / "diet_guard" / "firebase_auth.json",
|
|
"refresh-abc",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
minted: list[str] = []
|
|
|
|
def _mint(refresh_token: str) -> None:
|
|
minted.append(refresh_token)
|
|
|
|
result = recover_session(
|
|
config_root=tmp_path, app_name="screen_locker", mint=_mint
|
|
)
|
|
|
|
assert result.recovered is True
|
|
assert result.donor == "diet_guard"
|
|
assert minted == ["refresh-abc"]
|
|
|
|
def test_failure_explains_itself(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A failed recovery must carry a reason a human can act on."""
|
|
|
|
def _mint(_refresh_token: str) -> None:
|
|
message = "TOKEN_EXPIRED"
|
|
raise RuntimeError(message)
|
|
|
|
_write_cache(
|
|
tmp_path / "diet_guard" / "firebase_auth.json",
|
|
"refresh-abc",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
result = recover_session(
|
|
config_root=tmp_path, app_name="screen_locker", mint=_mint
|
|
)
|
|
|
|
assert result.recovered is False
|
|
assert "TOKEN_EXPIRED" in result.reason
|
|
|
|
def test_no_donor_is_reported_not_swallowed(self, tmp_path: Path) -> None:
|
|
""" "Nothing to borrow" must be a stated reason, never a silent False."""
|
|
result = recover_session(
|
|
config_root=tmp_path, app_name="screen_locker", mint=lambda _t: None
|
|
)
|
|
|
|
assert result.recovered is False
|
|
assert "no sibling" in result.reason.lower()
|