Harden runtime diagnostics for milestone 3
Make the milestone 3 runtime story predictable instead of treating doctor, self-check, and startup failures as loosely related surfaces. Split doctor and self-check into distinct read-only flows, add tri-state diagnostic status with stable IDs and next steps, and reuse that wording in CLI output, service logs, and tray-triggered diagnostics. Add non-mutating config/model probes, a make runtime-check gate, and public recovery/validation docs for the X11 GA roadmap. Validation: make runtime-check; PYTHONPATH=src python3 -m unittest discover -s tests -p 'test_*.py'; python3 -m py_compile src/*.py tests/*.py; PYTHONPATH=src python3 -m aman doctor --help; PYTHONPATH=src python3 -m aman self-check --help. Leave milestone 3 open in the roadmap until the manual X11 validation rows are filled.
This commit is contained in:
parent
a3368056ff
commit
ed1b59240b
16 changed files with 1298 additions and 248 deletions
|
|
@ -52,10 +52,17 @@ class _FakeDesktop:
|
|||
return
|
||||
|
||||
|
||||
class _HotkeyFailDesktop(_FakeDesktop):
|
||||
def start_hotkey_listener(self, hotkey, callback):
|
||||
_ = (hotkey, callback)
|
||||
raise RuntimeError("already in use")
|
||||
|
||||
|
||||
class _FakeDaemon:
|
||||
def __init__(self, cfg, _desktop, *, verbose=False):
|
||||
def __init__(self, cfg, _desktop, *, verbose=False, config_path=None):
|
||||
self.cfg = cfg
|
||||
self.verbose = verbose
|
||||
self.config_path = config_path
|
||||
self._paused = False
|
||||
|
||||
def get_state(self):
|
||||
|
|
@ -215,29 +222,58 @@ class AmanCliTests(unittest.TestCase):
|
|||
|
||||
def test_doctor_command_json_output_and_exit_code(self):
|
||||
report = DiagnosticReport(
|
||||
checks=[DiagnosticCheck(id="config.load", ok=True, message="ok", hint="")]
|
||||
checks=[DiagnosticCheck(id="config.load", status="ok", message="ok", next_step="")]
|
||||
)
|
||||
args = aman._parse_cli_args(["doctor", "--json"])
|
||||
out = io.StringIO()
|
||||
with patch("aman.run_diagnostics", return_value=report), patch("sys.stdout", out):
|
||||
with patch("aman.run_doctor", return_value=report), patch("sys.stdout", out):
|
||||
exit_code = aman._doctor_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
payload = json.loads(out.getvalue())
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual(payload["status"], "ok")
|
||||
self.assertEqual(payload["checks"][0]["id"], "config.load")
|
||||
|
||||
def test_doctor_command_failed_report_returns_exit_code_2(self):
|
||||
report = DiagnosticReport(
|
||||
checks=[DiagnosticCheck(id="config.load", ok=False, message="broken", hint="fix")]
|
||||
checks=[DiagnosticCheck(id="config.load", status="fail", message="broken", next_step="fix")]
|
||||
)
|
||||
args = aman._parse_cli_args(["doctor"])
|
||||
out = io.StringIO()
|
||||
with patch("aman.run_diagnostics", return_value=report), patch("sys.stdout", out):
|
||||
with patch("aman.run_doctor", return_value=report), patch("sys.stdout", out):
|
||||
exit_code = aman._doctor_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 2)
|
||||
self.assertIn("[FAIL] config.load", out.getvalue())
|
||||
self.assertIn("overall: fail", out.getvalue())
|
||||
|
||||
def test_doctor_command_warning_report_returns_exit_code_0(self):
|
||||
report = DiagnosticReport(
|
||||
checks=[DiagnosticCheck(id="model.cache", status="warn", message="missing", next_step="run aman once")]
|
||||
)
|
||||
args = aman._parse_cli_args(["doctor"])
|
||||
out = io.StringIO()
|
||||
with patch("aman.run_doctor", return_value=report), patch("sys.stdout", out):
|
||||
exit_code = aman._doctor_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("[WARN] model.cache", out.getvalue())
|
||||
self.assertIn("overall: warn", out.getvalue())
|
||||
|
||||
def test_self_check_command_uses_self_check_runner(self):
|
||||
report = DiagnosticReport(
|
||||
checks=[DiagnosticCheck(id="startup.readiness", status="ok", message="ready", next_step="")]
|
||||
)
|
||||
args = aman._parse_cli_args(["self-check", "--json"])
|
||||
out = io.StringIO()
|
||||
with patch("aman.run_self_check", return_value=report) as runner, patch("sys.stdout", out):
|
||||
exit_code = aman._self_check_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
runner.assert_called_once_with("")
|
||||
payload = json.loads(out.getvalue())
|
||||
self.assertEqual(payload["status"], "ok")
|
||||
|
||||
def test_bench_command_json_output(self):
|
||||
args = aman._parse_cli_args(["bench", "--text", "hello", "--repeat", "2", "--warmup", "0", "--json"])
|
||||
|
|
@ -583,6 +619,42 @@ class AmanCliTests(unittest.TestCase):
|
|||
self.assertTrue(path.exists())
|
||||
self.assertEqual(desktop.settings_invocations, 1)
|
||||
|
||||
def test_run_command_hotkey_failure_logs_actionable_issue(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "config.json"
|
||||
path.write_text(json.dumps({"config_version": 1}) + "\n", encoding="utf-8")
|
||||
args = aman._parse_cli_args(["run", "--config", str(path)])
|
||||
desktop = _HotkeyFailDesktop()
|
||||
with patch("aman._lock_single_instance", return_value=object()), patch(
|
||||
"aman.get_desktop_adapter", return_value=desktop
|
||||
), patch("aman.load", return_value=Config()), patch("aman.Daemon", _FakeDaemon), self.assertLogs(
|
||||
level="ERROR"
|
||||
) as logs:
|
||||
exit_code = aman._run_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
rendered = "\n".join(logs.output)
|
||||
self.assertIn("hotkey.parse: hotkey setup failed: already in use", rendered)
|
||||
self.assertIn("next_step: run `aman doctor --config", rendered)
|
||||
|
||||
def test_run_command_daemon_init_failure_logs_self_check_next_step(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "config.json"
|
||||
path.write_text(json.dumps({"config_version": 1}) + "\n", encoding="utf-8")
|
||||
args = aman._parse_cli_args(["run", "--config", str(path)])
|
||||
desktop = _FakeDesktop()
|
||||
with patch("aman._lock_single_instance", return_value=object()), patch(
|
||||
"aman.get_desktop_adapter", return_value=desktop
|
||||
), patch("aman.load", return_value=Config()), patch(
|
||||
"aman.Daemon", side_effect=RuntimeError("warmup boom")
|
||||
), self.assertLogs(level="ERROR") as logs:
|
||||
exit_code = aman._run_command(args)
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
rendered = "\n".join(logs.output)
|
||||
self.assertIn("startup.readiness: startup failed: warmup boom", rendered)
|
||||
self.assertIn("next_step: run `aman self-check --config", rendered)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue