feat(m9): systemd --user trigger modes + game-launch watcher — 0.23.0

D6 trigger modes, no root:
- core/service.py: write/enable `systemd --user` units; apply_mode(manual/
  always-on/game-launch) reconciles the recorder + watcher services; status().
- core/watcher.py + `rigdoctor watch`: poll Steam RunningAppID, auto-bracket a
  focused capture (D12 zero-config fallback; wrapper stays primary).
- CLI `rigdoctor service status|mode`; config `trigger_mode`.
- GUI Settings: "Recording trigger" dropdown (Apply runs apply_mode off-thread).
- Tests for unit generation, mode reconciliation, watcher transitions/parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:55:36 +02:00
parent e4a37176e1
commit bf3ac4af1a
11 changed files with 462 additions and 6 deletions
+58
View File
@@ -0,0 +1,58 @@
"""Tests for the M9 systemd --user trigger-mode service manager."""
import unittest
from unittest import mock
from rigdoctor.core import service
class UnitTextTests(unittest.TestCase):
def test_unit_text_has_required_sections(self):
txt = service.unit_text("RigDoctor recorder", ["record", "run"])
self.assertIn("[Unit]", txt)
self.assertIn("[Service]", txt)
self.assertIn("ExecStart=", txt)
self.assertIn("record run", txt)
self.assertIn("WantedBy=default.target", txt)
class ApplyModeTests(unittest.TestCase):
def test_unknown_mode_rejected(self):
ok, msg = service.apply_mode("turbo")
self.assertFalse(ok)
self.assertIn("Unknown", msg)
def test_no_systemd_saves_mode_but_reports(self):
with mock.patch.object(service, "available", return_value=False), \
mock.patch.object(service.config, "update_config") as update:
ok, msg = service.apply_mode("always-on")
self.assertFalse(ok)
self.assertIn("available", msg.lower())
update.assert_called_once_with(trigger_mode="always-on")
def test_always_on_enables_recorder_disables_watch(self):
calls = []
with mock.patch.object(service, "available", return_value=True), \
mock.patch.object(service, "install_units"), \
mock.patch.object(service, "_enable", side_effect=lambda n: calls.append(("enable", n)) or (0, "")), \
mock.patch.object(service, "_disable", side_effect=lambda n: calls.append(("disable", n)) or (0, "")), \
mock.patch.object(service.config, "update_config"):
ok, _ = service.apply_mode("always-on")
self.assertTrue(ok)
self.assertIn(("enable", service.RECORDER_UNIT), calls)
self.assertIn(("disable", service.WATCH_UNIT), calls)
def test_manual_disables_both(self):
disabled = []
with mock.patch.object(service, "available", return_value=True), \
mock.patch.object(service, "install_units"), \
mock.patch.object(service, "_enable", return_value=(0, "")), \
mock.patch.object(service, "_disable", side_effect=lambda n: disabled.append(n) or (0, "")), \
mock.patch.object(service.config, "update_config"):
ok, _ = service.apply_mode("manual")
self.assertTrue(ok)
self.assertEqual(set(disabled), {service.RECORDER_UNIT, service.WATCH_UNIT})
if __name__ == "__main__":
unittest.main()