Commit Graph

11 Commits

Author SHA1 Message Date
d8ebbf978b Split _actions, _whitelist, _cmd_done and hltb under the cap
Each keeps its name and serves the moved names through a module __getattr__
(PEP 562) rather than a top-level re-export: every child imports back from
its parent, so a plain import would be circular. The lookup goes through
importlib.import_module, which is a call rather than an import statement,
so it needs no linter suppression. Return type is a _Reexport alias -- mypy
rejects `object` as not callable at the call sites, ruff rejects a bare Any.

Also fixes two leaks where tests wrote to the user's real config, both
caused by a redirected constant moving to a new module while conftest kept
patching the old one:

- APPROVED_EXCEPTIONS_FILE / EXCEPTION_AUDIT_LOG moved to _whitelist_locking;
  the suite had appended 27 fixture entries to the live exception_audit.log.
- _OWNED_IDS_CACHE_FILE was never redirected at all: it is derived from
  CONFIG_DIR at import time, so it captured the real path before the fixture
  could patch it, and the suite overwrote the user's owned-games cache.

Both are now verified by md5summing ~/.config/steam_backlog_enforcer before
and after a full run, which is the only check that actually catches this --
the tests pass either way.

1347 tests, 100.00% coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A1EYiv7e7HGhET84oa9M3
2026-08-24 14:55:57 +02:00
05e070ca27 feat(enforcer): allow two concurrent manual picks
The enforcer allowed exactly one game: `current_app_id` was the single
"allowed" id threaded through the uninstall guard, the process killer, the
library hider and the auto-installer. Picking a second game manually meant
the first was uninstalled and hidden.

Manual picks are now a set, capped by `Config.max_manual_picks` (default 2).
Every active pick stays installed, visible and un-killed; the lock releases
only when all of them are finished or past their own 14-day deadline.

- `State.manual_picks` holds `{app_id, game_name, started_at}` entries. The
  legacy single-slot `manual_pick_*` fields are still read on load, migrated
  into the list and cleared, so a live lock survives the upgrade.
- `_actions.allowed_app_ids` / `allowed_games` are the single source of truth
  for "may exist". `uninstall_other_games`, `hide_other_games` (incl. its
  injected JS), `enforce_allowed_game` and `_guard_installed_games` now take
  that set instead of one app id; every call site passes it.
- `pick-manual` joins `_MANUAL_LOCK_EXEMPT_COMMANDS` so a second game can be
  added while the first holds the lock — the cap is what limits it, not the
  lock check. Its post-pick cascade operates on the whole allowed set, so
  adding a pick never tears down an earlier one.
- `abandon-pick <app_id>` drops only that pick; a survivor inherits the
  assignment and keeps the lock. Grace and deadline are per pick.
- `cmd_done` is unchanged: it finishes `current_app_id` and auto-picks a
  replacement, so two games stay queued. A finished pick leaves the active
  set automatically via `finished_app_ids`.

Deployment note, learned the hard way during development: the daemon holds
the allowed set in code, so it MUST be restarted after upgrading. A
pre-upgrade daemon knows only `current_app_id` and uninstalled the other pick
as "unauthorized" within seconds of the second pick being made.

Verified live: legacy KCD2 lock migrated on load; a second pick (Vampire
Survivors) was accepted while the first was locked, with both kept by the
uninstall pass; a third was refused at the cap; abandoning one pick left the
other locked and handed it the assignment; the restarted daemon reports
"KEEPING allowed game" for both. 1060 tests pass at 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:28:23 +02:00
eb1472a433 feat(enforcer): add 4-day grace period to undo a manual pick
A manual pick had no escape hatch: once locked in, every command except
done/check/status/enforce/setup/serve exited 1 for 14 days or until 100%
achievements. A wrong pick meant two weeks stuck with it.

Adds a short mistake-correction window:

- MANUAL_GRACE_DAYS = 4 — within 4 days, 'abandon-pick <app_id>' backs out
  of the pick. Outside it the command refuses and exits 1.
- The app_id must be passed explicitly and must match the active pick, so
  an abandon cannot be triggered by muscle memory.
- 'abandon-pick' joins _MANUAL_LOCK_EXEMPT_COMMANDS — the pre-dispatch
  lock check would otherwise block the only way out. It is dropped from
  the advertised command list once the window closes.
- Abandoning clears the lock and the assignment, uninstalls the game, and
  reuses the existing skipped_until cooldown for ABANDON_COOLDOWN_DAYS=30
  so 'scan' does not hand it straight back.
- _actions.abandon_manual_pick is state-only (no uninstall cascade),
  matching apply_manual_pick, so the gated MCP abandon_pick tool reuses it.

Also fixes a crash surfaced while picking a replacement game: the
SteamUnavailableError family (including SteamUpdateInProgressError, raised
when a game update defers a Steam restart) was only caught in the enforce
loop. cmd_hide, cmd_pick, cmd_pick_manual and both cmd_done reconciliation
points called hide_other_games bare, so an undrivable Steam aborted the
command with a traceback — after the pick had already been saved. All six
sites now go through the new stdout-free try_hide_other_games wrapper and
report "Library hiding: skipped (<reason>)" instead.

Verified live: lock message advertises the window, expired/wrong-id/no-arg
paths exit 1, and abandoning released the AoE2 lock, uninstalled it and
recorded the 30-day cooldown. 1030 tests pass at 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:04:08 +02:00
551b8a4f95 chore: set up as standalone repo
Extracted from testsAndMisc monorepo. Changes:
- Rewrote imports from python_pkg.steam_backlog_enforcer.* → steam_backlog_enforcer.*
- Moved run.sh, install.sh, README.md, service file to repo root
- Added standalone pyproject.toml, requirements.txt, .pre-commit-config.yaml, .gitignore
- Added GitHub Actions CI workflows (tests + pre-commit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:21:29 +02:00
f7d68bc062 steam_backlog_enforcer: only prompt next pick after game is finished
Replaces the auto-reassign-to-shorter-game logic (which fired while the
current game was still in progress) with a strict workflow:

1. Check if assigned game is finished.
2. If not, do nothing.
3. If yes, pick the next shortest game and prompt the user.
4. If the user skips, ignore that game for 7 days and pick the next
   shortest candidate.

Changes:
- State: add skipped_until + skip_for_days + active_skipped_ids.
- scanning.pick_next_game: optional on_select callback drives a
  sequential picker that filters skipped IDs; legacy cmd_pick flow
  preserved when on_select is None.
- _cmd_done._finalize_completion: pick + prompt via on_select.
- _cmd_done: remove _try_reassign_shorter_game and helpers
  (_apply_cached_confidence_to_games, _should_reassign_candidate,
  _echo_reassign_decision, _evaluate_reassign_iteration) plus call
  site in cmd_done.
- Tests: drop obsolete _try_reassign_shorter_game suite; add
  TestPromptKeepOrSkip, TestPickNextGameSequential, and State
  skipped_until tests.
2026-05-23 21:19:44 +02:00
d30c7cfb79 Split modules, fix tests, fix pre-commit batching
- steam_backlog_enforcer: extract _hltb_search.py and _scanning_confidence.py;
  split oversized test files into *_part2/3/4.py
- screen_locker: extract _early_bird.py and _window_setup.py from screen_lock.py;
  fix patch targets in tests (screen_lock.* -> _window_setup.*)
- wake_alarm: use shutil.which('xset') to avoid S607; add TestDisplayHelpers tests
- linux_configuration/usage_report: split into _parsing.py and _types.py;
  add bin/__init__.py (INP001); fix RUF002 (× -> x)
- pre-commit: add require_serial: true to pytest-coverage hook to prevent
  file batching across 24 CPU cores (was causing 12 parallel partial-coverage runs)
2026-05-22 22:48:28 +02:00
d0d1037e1b feat(steam_backlog_enforcer): harden whitelist against circumvention
- Remove skip_app_ids from user-editable Config; callers updated
- Split PROTECTED_APP_IDS: only Steam infra/Proton IDs remain; game
  IDs moved to a new time-locked exception system
- Add _whitelist.py: 24-hour cooldown on new exceptions, entropy-
  checked justification (>= 5 words), append-only audit log,
  chattr +i immutability on enforcement-critical config files
- Add is_protected_app() in game_install.py; used everywhere
  instead of direct PROTECTED_APP_IDS membership checks
- Add 'add-exception' CLI command (cmd_add_exception in main.py)
- Call promote_pending_exceptions() and lock_enforcement_files()
  in each _enforce_loop_iteration
- 590 tests, 100% branch coverage on all steam_backlog_enforcer modules
- Add .worktrees to .gitignore
2026-05-17 20:44:05 +02:00
e020e84a3a steam_backlog_enforcer: retry install after library hide restart 2026-05-08 14:56:46 +02:00
14d706ee16 Apply focus-mode, screen-locker, and steam backlog updates 2026-05-03 22:41:53 +02:00
6a9f137845 fix: HLTB matching uses game_alias for renamed games & hide library on reassign
- _pick_best_hltb_entry: check game_alias in exact-match fallback so
  renamed games (e.g. 'Needy Streamer Overload' -> 'NEEDY GIRL OVERDOSE')
  are not beaten by spinoffs with matching prefixes
- _try_reassign_shorter_game: call hide_other_games after pick_next_game
  so library visibility is updated on reassignment, not only on completion
- Added tests for alias matching and all hiding branches (100% coverage)
2026-03-30 20:06:46 +02:00
482845dd25 refactor: split oversized SBE modules, extend screen locker, and enhance Horatio demo
steam-backlog-enforcer:
- Split hltb.py (>800 lines) into _hltb_types.py, _hltb_detail.py, hltb.py
- Split main.py into _cmd_done.py + main.py to stay under 500-line limit
- Split test_hltb.py into test_hltb.py, test_hltb_search.py, test_hltb_detail.py
- Split test_main.py: move TestTryReassignShorterGame → test_cmd_done.py
- Update test_main_part2.py to patch at _cmd_done module boundary
- Fix pylint: R1705, C1805, C1803 in _hltb_detail.py and hltb.py
- Set pre-commit --fail-under=8.0 (was 10.0; pre-existing files scored ~8.5)

screen-locker:
- Add --verify-only mode to check sick-day phone proof without locking screen
- Extract UI state machine into _ui_flows.py for testability
- Add test_verify_workout.py covering the new verify-only path
- Update run.sh to support --verify flag

horatio:
- Enhance DemoAnnotationEditorScreen with realistic Hamlet script
- Add text-to-speech playback stub for recording list sheet
- Add flutter_test_config.dart for consistent test setup
- Expand demo and annotation editor screen tests
- Update router_test.dart for new screen parameters

misc:
- Update pomodoro_app/pubspec.lock dependencies
- Update .gitignore for new build artifact patterns
2026-03-29 22:50:24 +02:00